From 6b9475d16a248fcc4d26cbb4e722bb429f8d1f01 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 25 Feb 2022 10:01:27 +0100 Subject: [PATCH 001/122] keysight issue --- qcodes/instrument_drivers/keysight/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 qcodes/instrument_drivers/keysight/__init__.py diff --git a/qcodes/instrument_drivers/keysight/__init__.py b/qcodes/instrument_drivers/keysight/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 From bfc330e54c65497d17eae89329fed07a7ccf2a23 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 25 Feb 2022 10:10:49 +0100 Subject: [PATCH 002/122] Auto stash before merge of "master" and "origin/master" --- qcodes/monitor/monitor.py | 64 ++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/qcodes/monitor/monitor.py b/qcodes/monitor/monitor.py index 9dbe6f1c241..80cc80311da 100644 --- a/qcodes/monitor/monitor.py +++ b/qcodes/monitor/monitor.py @@ -39,6 +39,7 @@ Optional, Sequence, Union, + List ) import websockets @@ -62,7 +63,9 @@ def _get_metadata( - *parameters: Parameter, use_root_instrument: bool = True + *parameters: Parameter, + use_root_instrument: bool = True, + parameters_metadata: Dict[Union[Parameter, str], dict] ) -> Dict[str, Any]: """ Return a dictionary that contains the parameter metadata grouped by the @@ -71,28 +74,56 @@ def _get_metadata( metadata_timestamp = time.time() # group metadata by instrument metas: Dict[Any, Any] = defaultdict(list) + + # Ensure each element of parameters_metadata is a dict and not something else like a DotDict + parameters_metadata = {key: dict(val) for key, val in parameters_metadata.items()} + for parameter in parameters: + # Get potential parameter metadata describing how to process parameter + if parameter in parameters_metadata: + parameter_metadata = parameters_metadata[parameter] + elif parameter.name in parameters_metadata: + parameter_metadata = parameters_metadata[parameter.name] + elif parameter.full_name in parameters_metadata: + parameter_metadata = parameters_metadata[parameter.full_name] + else: + parameter_metadata = {} + # Get the latest value from the parameter, # respecting the max_val_age parameter meta: Dict[str, Optional[Union[float, str]]] = {} - meta["value"] = str(parameter.get_latest()) + value = parameter.get_latest() + + # Apply a modifier if provided by metadata + if 'modifier' in parameter_metadata: + value = parameter_metadata['modifier'](value) + if 'scale' in parameter_metadata: + value = value * parameter_metadata['scale'] + + # Format value, usually to a string unless specified in parameter_metadata['formatter'] + formatter = parameter_metadata.get('formatter', '{}') + meta["value"] = formatter.format(value) + timestamp = parameter.get_latest.get_timestamp() if timestamp is not None: meta["ts"] = timestamp.timestamp() else: meta["ts"] = None - meta["name"] = parameter.label or parameter.name - meta["unit"] = parameter.unit + meta["name"] = parameter_metadata.get('name') or parameter.label or parameter.name + meta["unit"] = parameter_metadata.get('unit') or parameter.unit # find the base instrument that this parameter belongs to if use_root_instrument: baseinst = parameter.root_instrument else: baseinst = parameter.instrument - if baseinst is None: - metas["Unbound Parameter"].append(meta) + + if 'group' in parameter_metadata: + metas[parameter_metadata['group']].append(meta) + elif baseinst is not None: + metas[str(parameter.root_instrument)].append(meta) else: - metas[str(baseinst)].append(meta) + metas["Unbound Parameter"].append(meta) # Create list of parameters, grouped by instrument parameters_out = [] @@ -105,7 +136,10 @@ def _get_metadata( def _handler( - parameters: Sequence[Parameter], interval: float, use_root_instrument: bool = True + parameters: Sequence[Parameter], + interval: float, + use_root_instrument: bool = True, + parameters_metadata: Dict[Union[Parameter, str], dict] ) -> Callable[["WebSocketServerProtocol", str], Awaitable[None]]: """ Return the websockets server handler. @@ -120,7 +154,9 @@ async def server_func(websocket: "WebSocketServerProtocol", _: str) -> None: # Update the parameter values try: meta = _get_metadata( - *parameters, use_root_instrument=use_root_instrument + *parameters, + use_root_instrument=use_root_instrument, + parameters_metadata=parameters_metadata ) except ValueError: log.exception("Error getting parameters") @@ -149,6 +185,8 @@ def __init__( *parameters: Parameter, interval: float = 1, use_root_instrument: bool = True, + parameters_metadata: Optional[Dict[Union[Parameter, str], dict]] = None, + daemon: bool = True ): """ Monitor qcodes parameters. @@ -159,7 +197,7 @@ def __init__( use_root_instrument: Defines if parameters are grouped according to parameter.root_instrument or parameter.instrument """ - super().__init__() + super().__init__(daemon=daemon) # Check that all values are valid parameters for parameter in parameters: @@ -170,10 +208,14 @@ def __init__( self.loop: Optional[asyncio.AbstractEventLoop] = None self.server: Optional["WebSocketServer"] = None self._parameters = parameters + self._parameters_metadata = parameters_metadata self.loop_is_closed = Event() self.server_is_started = Event() self.handler = _handler( - parameters, interval=interval, use_root_instrument=use_root_instrument + parameters, + interval=interval, + use_root_instrument=use_root_instrument, + parameters_metadata=parameters_metadata or {} ) log.debug("Start monitoring thread") From a6aceadb82075844ca3bf809500ce36416fb6aad Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Sun, 6 Mar 2022 18:50:57 +0100 Subject: [PATCH 003/122] monitor message --- qcodes/monitor/monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/monitor/monitor.py b/qcodes/monitor/monitor.py index 80cc80311da..5a844fc23d6 100644 --- a/qcodes/monitor/monitor.py +++ b/qcodes/monitor/monitor.py @@ -139,7 +139,7 @@ def _handler( parameters: Sequence[Parameter], interval: float, use_root_instrument: bool = True, - parameters_metadata: Dict[Union[Parameter, str], dict] + parameters_metadata: Dict[Union[Parameter, str], dict] = {} ) -> Callable[["WebSocketServerProtocol", str], Awaitable[None]]: """ Return the websockets server handler. From 88757c2a4d94aec2ecb7fa462dc892f60322795a Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Sat, 2 Apr 2022 09:35:44 +0200 Subject: [PATCH 004/122] fix: ensure keyboardinterrupt doesn't interrupt visa command --- qcodes/instrument_drivers/QDevil/QDevil_QDAC.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/qcodes/instrument_drivers/QDevil/QDevil_QDAC.py b/qcodes/instrument_drivers/QDevil/QDevil_QDAC.py index 87028551ec5..9638ac4cc1b 100644 --- a/qcodes/instrument_drivers/QDevil/QDevil_QDAC.py +++ b/qcodes/instrument_drivers/QDevil/QDevil_QDAC.py @@ -21,6 +21,7 @@ from qcodes.instrument.parameter import ParamRawDataType from qcodes.instrument.visa import VisaInstrument from qcodes.utils import validators as vals +from qcodes.utils.delaykeyboardinterrupt import DelayedKeyboardInterrupt LOG = logging.getLogger(__name__) @@ -788,9 +789,12 @@ def write(self, cmd: str) -> None: """ LOG.debug(f"Writing to instrument {self.name}: {cmd}") - self.visa_handle.write(cmd) - for _ in range(cmd.count(';')+1): - self._write_response = self.visa_handle.read() + + with DelayedKeyboardInterrupt(): + self.visa_handle.write(cmd) + for _ in range(cmd.count(';')+1): + self._write_response = self.visa_handle.read() + def read(self) -> str: return self.visa_handle.read() From ffb444bf8054c789599b87a07dc046529aab7bf4 Mon Sep 17 00:00:00 2001 From: Serwan Date: Sat, 2 Apr 2022 10:30:10 +0200 Subject: [PATCH 005/122] Begun incorporating measurement loop --- qcodes/dataset/measurement_loop.py | 1331 ++++++++++++++++++++++++++++ qcodes/utils/helpers.py | 123 +++ 2 files changed, 1454 insertions(+) create mode 100644 qcodes/dataset/measurement_loop.py diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py new file mode 100644 index 00000000000..df918c38bba --- /dev/null +++ b/qcodes/dataset/measurement_loop.py @@ -0,0 +1,1331 @@ +import numpy as np +from typing import List, Tuple, Union, Sequence, Dict, Any, Callable, Iterable +import threading +from time import sleep, perf_counter +import traceback +import logging +from datetime import datetime + +from qcodes.station import Station +from qcodes.data.data_set import new_data, DataSet +from qcodes.data.data_array import DataArray +from qcodes.instrument.sweep_values import SweepValues +from qcodes.instrument.parameter import Parameter, MultiParameter +from qcodes.utils.helpers import ( + using_ipython, + directly_executed_from_cell, + get_last_input_cells, + PerformanceTimer +) +from qcodes import config as qcodes_config + +RAW_VALUE_TYPES = (float, int, bool, np.ndarray, np.integer, np.floating, np.bool_, type(None)) + + +class DataHandler: + def __init__(self, measurement_loop): + self.measurement_loop = measurement_loop + self.datasets = [] + + def create_dataset(self): + pass + + def new_dataset(self): + pass + + def add_result(self, parameter, result, action_indices, loop_indices, loop_shape, setpoints): + pass + + +class MeasurementLoop: + """Class to perform measurements + + Args: + name: Measurement name, also used as the dataset name + force_cell_thread: Enforce that the measurement has been started from a + separate thread if it has been directly executed from an IPython + cell/prompt. This is because a measurement is usually run from a + separate thread using the magic command `%%new_job`. + An error is raised if this has not been satisfied. + Note that if the measurement is started within a function, no error + is raised. + notify: Notify when measurement is complete. + The function `Measurement.notify_function` must be set + + + Notes: + When the Measurement is started in a separate thread (using %%new_job), + the Measurement is registered in the user namespace as 'msmt', and the + dataset as 'data' + + """ + + # Context manager + running_measurement = None + measurement_thread = None + + # Default names for measurement and dataset, used to set user namespace + # variables if measurement is executed in a separate thread. + _default_measurement_name = "msmt" + _default_dataset_name = "data" + final_actions = [] + except_actions = [] + max_arrays = 100 + + _t_start = None + + # Notification function, called if notify=True. + # Function should receive the following arguments: + # Measurement object, exception_type, exception_message, traceback + # The last three are only not None if an error has occured + notify_function = None + + def __init__(self, name: str, force_cell_thread: bool = True, notify=False): + self.name = name + + # Dataset is created during `with Measurement('name')` + # TODO option to use multiple datasets + self.dataset = None + + # Total dimensionality of loop + self.loop_shape: Union[Tuple[int], None] = None + + # Current loop indices + self.loop_indices: Union[Tuple[int], None] = None + + # Index of current action + self.action_indices: Union[Tuple[int], None] = None + + # contains data groups, such as ParameterNodes and nested measurements + self._data_groups: Dict[Tuple[int], "MeasurementLoop"] = {} + + # Registry of actions: sweeps, measurements, and data groups + self.actions: Dict[Tuple[int], Any] = {} + self.action_names: Dict[Tuple[int], str] = {} + + self.is_context_manager: bool = False # Whether used as context manager + self.is_paused: bool = False # Whether the Measurement is paused + self.is_stopped: bool = False # Whether the Measurement is stopped + + self.notify = notify + + self.force_cell_thread = force_cell_thread and using_ipython() + + # Each measurement can have its own final actions, to be executed + # regardless of whether the measurement finished successfully or not + # Note that there are also Measurement.final_actions, which are always + # executed when the outermost measurement finishes + self.final_actions = [] + self.except_actions = [] + self._masked_properties = [] + + self.timings = PerformanceTimer() + + def log(self, message: str, level="info"): + """Send a log message + + Args: + message: Text to log + level: Logging level (debug, info, warning, error) + """ + assert level in ["debug", "info", "warning", "error"] + logger = logging.getLogger("msmt") + log_function = getattr(logger, level) + + # Append measurement name + if self.name is not None: + message += f" - {self.name}" + + log_function(message) + + @property + def data_groups(self) -> Dict[Tuple[int], "MeasurementLoop"]: + if running_measurement() is not None: + return running_measurement()._data_groups + else: + return self._data_groups + + @property + def active_action(self): + return self.actions.get(self.action_indices, None) + + @property + def active_action_name(self): + return self.action_names.get(self.action_indices, None) + + def __enter__(self): + """Operation when entering a loop""" + self.is_context_manager = True + + # Encapsulate everything in a try/except to ensure that the context + # manager is properly exited. + try: + if MeasurementLoop.running_measurement is None: + # Register current measurement as active primary measurement + MeasurementLoop.running_measurement = self + MeasurementLoop.measurement_thread = threading.current_thread() + + # Initialize dataset + self.data_handler = DataHandler() + self.dataset = new_data(name=self.name) + + self._initialize_metadata(self.dataset) + with self.timings.record(['dataset', 'save_metadata']): + self.dataset.save_metadata() + + if hasattr(self.dataset, 'save_config'): + self.dataset.save_config() + + # Initialize attributes + self.loop_shape = () + self.loop_indices = () + self.action_indices = (0,) + self.data_arrays = {} + self.set_arrays = {} + + self.log(f'Measurement started {self.dataset.location}') + print(f'Measurement started {self.dataset.location}') + + else: + if threading.current_thread() is not MeasurementLoop.measurement_thread: + raise RuntimeError( + "Cannot run a measurement while another measurement " + "is already running in a different thread." + ) + + # Primary measurement is already running. Add this measurement as + # a data_group of the primary measurement + msmt = MeasurementLoop.running_measurement + msmt.data_groups[msmt.action_indices] = self + data_groups = [ + (key, getattr(val, 'name', 'None')) for key, val in msmt.data_groups.items() + ] + msmt.dataset.add_metadata({'data_groups': data_groups}) + msmt.action_indices += (0,) + + # Nested measurement attributes should mimic the primary measurement + self.loop_shape = msmt.loop_shape + self.loop_indices = msmt.loop_indices + self.action_indices = msmt.action_indices + self.data_arrays = msmt.data_arrays + self.set_arrays = msmt.set_arrays + self.timings = msmt.timings + + # Perform measurement thread check, and set user namespace variables + if self.force_cell_thread and MeasurementLoop.running_measurement is self: + # Raise an error if force_cell_thread is True and the code is run + # directly from an IPython cell/prompt but not from a separate thread + is_main_thread = threading.current_thread() == threading.main_thread() + if is_main_thread and directly_executed_from_cell(): + raise RuntimeError( + "Measurement must be created in dedicated thread. " + "Otherwise specify force_thread=False" + ) + + # Register the Measurement and data as variables in the user namespace + # Usually as variable names are 'msmt' and 'data' respectively + from IPython import get_ipython + + shell = get_ipython() + shell.user_ns[self._default_measurement_name] = self + shell.user_ns[self._default_dataset_name] = self.dataset + + + return self + except: + # An error has occured, ensure running_measurement is cleared + if MeasurementLoop.running_measurement is self: + MeasurementLoop.running_measurement = None + raise + + def __exit__(self, exc_type: Exception, exc_val, exc_tb): + """Operation when exiting a loop + + Args: + exc_type: Type of exception, None if no exception + exc_val: Exception message, None if no exception + exc_tb: Exception traceback object, None if no exception + """ + msmt = MeasurementLoop.running_measurement + if msmt is self: + # Immediately unregister measurement as main measurement, in case + # an error occurs during final actions. + MeasurementLoop.running_measurement = None + + if exc_type is not None: + self.log(f"Measurement error {exc_type.__name__}({exc_val})", level="error") + + self._apply_actions(self.except_actions, label="except", clear=True) + + if msmt is self: + self._apply_actions( + MeasurementLoop.except_actions, label="global except", clear=True + ) + + self._apply_actions(self.final_actions, label="final", clear=True) + + self.unmask_all() + + if msmt is self: + # Also perform global final actions + # These are always performed when outermost measurement finishes + self._apply_actions(MeasurementLoop.final_actions, label="global final") + + # Notify that measurement is complete + if self.notify and self.notify_function is not None: + try: + self.notify_function(exc_type, exc_val, exc_tb) + except: + self.log("Could not notify", level="error") + + t_stop = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + self.dataset.add_metadata({"t_stop": t_stop}) + self.dataset.add_metadata({"timings": self.timings}) + + # If dataset only contains setpoints, don't finalize dataset. + if not all([arr.is_setpoint for arr in self.dataset.arrays.values()]): + # Sadly the timing to finalize the dataset won't be stored in the metadata. + with self.timings.record(['dataset', 'finalize']): + self.dataset.finalize() + self.dataset.active = False + else: + if hasattr(self.dataset.formatter, 'close_file'): + self.dataset.formatter.close_file(self) + self.dataset.save_metadata() + + self.dataset.active = False + + self.log(f'Measurement finished {self.dataset.location}') + + else: + msmt.step_out(reduce_dimension=False) + + self.is_context_manager = False + + def _initialize_metadata(self, dataset: DataSet = None): + """Initialize dataset metadata""" + if dataset is None: + dataset = self.dataset + + config = qcodes_config.get('user', {}).get('silq_config', qcodes_config) + dataset.add_metadata({"config": config}) + + dataset.add_metadata({"measurement_type": "Measurement"}) + + # Add instrument information + if Station.default is not None: + dataset.add_metadata({"station": Station.default.snapshot()}) + + if using_ipython(): + measurement_cell = get_last_input_cells(1)[0] + + measurement_code = measurement_cell + # If the code is run from a measurement thread, there is some + # initial code that should be stripped + init_string = "get_ipython().run_cell_magic('new_job', '', " + if measurement_code.startswith(init_string): + measurement_code = measurement_code[len(init_string) + 1 : -4] + + self._t_start = datetime.now() + dataset.add_metadata( + { + "measurement_cell": measurement_cell, + "measurement_code": measurement_code, + "last_input_cells": get_last_input_cells(20), + "t_start": self._t_start.strftime('%Y-%m-%d %H:%M:%S') + } + ) + + # Data array functions + def _create_data_array( + self, + action_indices: Tuple[int], + result, + parameter: Parameter = None, + is_setpoint: bool = False, + name: str = None, + label: str = None, + unit: str = None, + ): + """Create a data array from a parameter and result. + + The data array shape is extracted from the result shape, and the current + loop dimensions. + + The data array is added to the current data set. + + Args: + parameter: Parameter for which to create a DataArray. Can also be a + string, in which case it is the data_array name + result: Result returned by the Parameter + action_indices: Action indices for which to store parameter + is_setpoint: Whether the Parameter is used for sweeping or measuring + label: Data array label. If not provided, the parameter label is + used. If the parameter is a name string, the label is extracted + from the name. + unit: Data array unit. If not provided, the parameter unit is used. + + Returns: + Newly created data array + + """ + if parameter is None and name is None: + raise SyntaxError( + "When creating a data array, must provide either a parameter or a name" + ) + + if len(running_measurement().data_arrays) >= self.max_arrays: + raise RuntimeError( + f"Number of arrays in dataset exceeds " + f"Measurement.max_arrays={self.max_arrays}. Perhaps you forgot" + f"to encapsulate a loop with a Sweep()?" + ) + + array_kwargs = { + "is_setpoint": is_setpoint, + "action_indices": action_indices, + "shape": self.loop_shape, + } + + if is_setpoint or isinstance(result, (np.ndarray, list)): + array_kwargs["shape"] += np.shape(result) + + # Use dummy index (1, ) if measurement is performed outside a Sweep + if not array_kwargs["shape"]: + array_kwargs["shape"] = (1,) + + if isinstance(parameter, Parameter): + array_kwargs["parameter"] = parameter + # Add a custom name + if name is not None: + array_kwargs["full_name"] = name + if label is not None: + array_kwargs["label"] = label + if unit is not None: + array_kwargs["unit"] = unit + else: + array_kwargs["name"] = name + if label is None: + label = name[0].capitalize() + name[1:].replace("_", " ") + array_kwargs["label"] = label + array_kwargs["unit"] = unit or "" + + # Add setpoint arrays + if not is_setpoint: + array_kwargs["set_arrays"] = self._add_set_arrays( + action_indices, result, parameter=parameter, name=(name or parameter.name) + ) + + data_array = DataArray(**array_kwargs) + + data_array.array_id = data_array.full_name + data_array.array_id += "_" + "_".join(str(k) for k in action_indices) + + data_array.init_data() + + self.dataset.add_array(data_array) + with self.timings.record(['dataset', 'save_metadata']): + self.dataset.save_metadata() + + # Add array to set_arrays or to data_arrays of this Measurement + if is_setpoint: + self.set_arrays[action_indices] = data_array + else: + self.data_arrays[action_indices] = data_array + + return data_array + + def _add_set_arrays( + self, action_indices: Tuple[int], result, name: str, parameter: Union[Parameter, None] = None + ): + """Create set arrays for a given action index""" + set_arrays = [] + for k in range(1, len(action_indices)): + sweep_indices = action_indices[:k] + + if sweep_indices in self.set_arrays: + set_arrays.append(self.set_arrays[sweep_indices]) + # TODO handle grouped arrays (e.g. ParameterNode, nested Measurement) + # Create new set array(s) if parameter result is an array or list + if isinstance(result, (np.ndarray, list)): + if isinstance(result, list): + result = np.ndarray(result) + + for k, shape in enumerate(result.shape): + arr = np.arange(shape) + label = None + unit = None + if parameter is not None and hasattr(parameter, 'setpoints') \ + and parameter.setpoints is not None: + arr_idx = parameter.names.index(name) + arr = parameter.setpoints[arr_idx][k] + label = parameter.setpoint_labels[arr_idx][k] + unit = parameter.setpoint_units[arr_idx][k] + + # Add singleton dimensions + arr = np.broadcast_to(arr, result.shape[: k + 1]) + + set_array = self._create_data_array( + action_indices=action_indices + (0,) * k, + result=arr, + name=f"{name}_set{k}", + label=label, + unit=unit, + is_setpoint=True, + ) + set_arrays.append(set_array) + + # Add a dummy array in case the measurement was performed outside of + # a Sweep. This is not needed if the result is an array + if not set_arrays and not self.loop_indices: + set_arrays = [ + self._create_data_array( + action_indices=running_measurement().action_indices, + result=result, + name="None", + is_setpoint=True, + ) + ] + set_arrays[0][0] = 1 + + return tuple(set_arrays) + + def get_arrays(self, action_indices: Sequence[int] = None) -> List[DataArray]: + """Get all arrays belonging to the current action indices + + If the action indices corresponds to a group of arrays (e.g. a nested + measurement or ParameterNode), all the arrays in the group are returned + + Args: + action_indices: Action indices of arrays. + If not provided, the current action_indices are chosen + + Returns: + List of data arrays matching the action indices + """ + if action_indices is None: + action_indices = self.action_indices + + if not isinstance(action_indices, Sequence): + raise SyntaxError("parent_action_indices must be a tuple") + + num_indices = len(action_indices) + return [ + arr + for action_indices, arr in self.data_arrays.items() + if action_indices[:num_indices] == action_indices + ] + + def _verify_action(self, action, name, add_if_new=True): + """Verify an action corresponds to the current action indices. + + This is only relevant if an action has previously been performed at + these action indices + """ + if self.action_indices not in self.actions: + if add_if_new: + # Add current action to action registry + self.actions[self.action_indices] = action + self.action_names[self.action_indices] = name + elif name != self.action_names[self.action_indices]: + raise RuntimeError( + f"Wrong measurement at action_indices {self.action_indices}. " + f"Expected: {self.action_names[self.action_indices]}. Received: {name}" + ) + + def _add_measurement_result( + self, + action_indices, + result, + parameter=None, + store: bool = True, + name: str = None, + label: str = None, + unit: str = None, + ): + """Store single measurement result + + This method is called from type-specific methods, such as + ``_measure_value``, ``_measure_parameter``, etc. + """ + if parameter is None and name is None: + raise SyntaxError( + "When adding a measurement result, must provide either a " + "parameter or name" + ) + + # Get parameter data array, creating a new one if necessary + if action_indices not in self.data_arrays: + # Create array based on first result type and shape + self._create_data_array( + action_indices, + result, + parameter=parameter, + name=name, + label=label, + unit=unit, + ) + + # Select existing array + data_array = self.data_arrays[action_indices] + + # Ensure an existing data array has the correct name + # parameter can also be a string, in which case we don't use parameter.name + if name is None: + name = parameter.name + + # TODO is this the right place for this check? + if not data_array.name == name: + raise SyntaxError( + f"Existing DataArray '{data_array.name}' differs from result {name}" + ) + + data_to_store = {data_array.array_id: result} + + # If result is an array, update set_array elements + if isinstance(result, list): # Convert result list to array + result = np.ndarray(result) + if isinstance(result, np.ndarray): + ndim = len(self.loop_indices) + if len(data_array.set_arrays) != ndim + result.ndim: + raise RuntimeError( + f"Wrong number of set arrays for {data_array.name}. " + f"Expected {ndim + result.ndim} instead of " + f"{len(data_array.set_arrays)}." + ) + + for k, set_array in enumerate(data_array.set_arrays[ndim:]): + # Successive set arrays must increase dimensionality by unity + arr = np.arange(result.shape[k]) + if parameter is not None and hasattr(parameter, 'setpoints') \ + and parameter.setpoints is not None: + arr_idx = parameter.names.index(name) + arr = parameter.setpoints[arr_idx][k] + + # Add singleton dimensions + arr = np.broadcast_to(arr, result.shape[: k + 1]) + data_to_store[set_array.array_id] = arr + + # Use dummy index if there are no loop indices. + # This happens if the measurement is performed outside a Sweep + loop_indices = self.loop_indices + if not loop_indices and not isinstance(result, (list, np.ndarray)): + loop_indices = (0,) + + if store: + with self.timings.record(['dataset', 'store']): + self.dataset.store(loop_indices, data_to_store) + + return data_to_store + + def _apply_actions(self, actions: list, label="", clear=False): + """Apply actions, either except_actions or final_actions""" + for action in actions: + try: + action() + except Exception as e: + self.log( + f"Could not execute {label} action {action} \n" + f"{traceback.format_exc()}", + level="error", + ) + + if clear: + actions.clear() + + # Measurement-related functions + def _measure_parameter(self, parameter, name=None, label=None, unit=None, **kwargs): + """Measure parameter and store results. + + Called from `measure`. + MultiParameter is called separately. + """ + name = name or parameter.name + + # Ensure measuring parameter matches the current action_indices + self._verify_action(action=parameter, name=name, add_if_new=True) + + # Get parameter result + result = parameter(**kwargs) + + self._add_measurement_result( + self.action_indices, + result, + parameter=parameter, + name=name, + label=label, + unit=unit, + ) + + return result + + def _measure_multi_parameter(self, multi_parameter, name=None, **kwargs): + """Measure MultiParameter and store results + + Called from `measure` + + Notes: + - Does not store setpoints yet + """ + name = name or multi_parameter.name + + # Ensure measuring multi_parameter matches the current action_indices + self._verify_action(action=multi_parameter, name=name, add_if_new=True) + + with self.timings.record(['measurement', self.action_indices, 'get']): + results_list = multi_parameter(**kwargs) + + results = dict(zip(multi_parameter.names, results_list)) + + if name is None: + name = multi_parameter.name + + with MeasurementLoop(name) as msmt: + for k, (key, val) in enumerate(results.items()): + msmt.measure( + val, + name=key, + parameter=multi_parameter, + label=multi_parameter.labels[k], + unit=multi_parameter.units[k], + ) + + return results + + def _measure_callable(self, callable, name=None, **kwargs): + """Measure a callable (function) and store results + + The function should return a dict, from which each item is measured. + If the function already contains creates a Measurement, the return + values aren't stored. + """ + # Determine name + if name is None: + if hasattr(callable, "__self__") and isinstance( + callable.__self__, ParameterNode + ): + name = callable.__self__.name + elif hasattr(callable, "__name__"): + name = callable.__name__ + else: + action_indices_str = "_".join(str(idx) for idx in self.action_indices) + name = f"data_group_{action_indices_str}" + + # Ensure measuring callable matches the current action_indices + self._verify_action(action=callable, name=name, add_if_new=True) + + # Record action_indices before the callable is called + action_indices = self.action_indices + + results = callable(**kwargs) + + # Check if the callable already performed a nested measurement + # In this case, the nested measurement is stored as a data_group, and + # has loop indices corresponding to the current ones. + msmt = MeasurementLoop.running_measurement + data_group = msmt.data_groups.get(action_indices) + if getattr(data_group, "loop_indices", None) != self.loop_indices: + # No nested measurement has been performed in the callable. + # Add results, which should be dict, by creating a nested measurement + if not isinstance(results, dict): + raise SyntaxError(f"{name} results must be a dict, not {results}") + + with MeasurementLoop(name) as msmt: + for key, val in results.items(): + msmt.measure(val, name=key) + + return results + + def _measure_dict(self, value: dict, name: str): + """Store dictionary results + + Each key is an array name, and the value is the value to store + """ + if not isinstance(value, dict): + raise SyntaxError(f"{name} must be a dict, not {value}") + + if not isinstance(name, str) or name == "": + raise SyntaxError(f"Dict result {name} must have a valid name: {value}") + + # Ensure measuring callable matches the current action_indices + self._verify_action(action=None, name=name, add_if_new=True) + + with MeasurementLoop(name) as msmt: + for key, val in value.items(): + msmt.measure(val, name=key) + + return value + + def _measure_value(self, value, name, parameter=None, label=None, unit=None): + """Store a single value (float/int/bool) + + If this value comes from another parameter acquisition, e.g. from a + MultiParameter, the parameter can be passed to use the right set arrays. + """ + if name is None: + raise RuntimeError("Must provide a name when measuring a value") + + # Ensure measuring callable matches the current action_indices + self._verify_action(action=None, name=name, add_if_new=True) + + if isinstance(value, np.integer): + value = int(value) + elif isinstance(value, np.floating): + value = float(value) + elif isinstance(value, np.bool_): + value = bool(value) + + result = value + self._add_measurement_result( + action_indices=self.action_indices, + result=result, + parameter=parameter, + name=name, + label=label, + unit=unit, + ) + return result + + def measure( + self, + measurable: Union[ + Parameter, Callable, dict, float, int, bool, np.ndarray, None + ], + name=None, + *, # Everything after here must be a kwarg + label=None, + unit=None, + timestamp=False, + **kwargs, + ): + """Perform a single measurement of a Parameter, function, etc. + + + Args: + measurable: Item to measure. Can be one of the following: + Parameter + Callable function/method, which should either perform a nested + Measurement, or return a dict. + In the case of returning a dict, all the key/value pairs + are grouped together. + float, int, bool, array + name: Optional name for measured element or data group. + If the measurable is a float, int, bool, or array, the name is + mandatory. + Otherwise, the default name is used. + label: Optional label, is ignored if measurable is a Parameter or callable + unit: Optional unit, is ignored if measurable is a Parameter or callable + timestamp: If True, the timestamps immediately before and after this + measurement are recorded + + Returns: + Return value of measurable + """ + if not self.is_context_manager: + raise RuntimeError( + "Must use the Measurement as a context manager, " + "i.e. 'with Measurement(name) as msmt:'" + ) + elif self.is_stopped: + raise SystemExit("Measurement.stop() has been called") + elif threading.current_thread() is not MeasurementLoop.measurement_thread: + raise RuntimeError( + "Cannot measure while another measurement is already running " + "in a different thread." + ) + + if self != MeasurementLoop.running_measurement: + # Since this Measurement is not the running measurement, it is a + # DataGroup in the running measurement. Delegate measurement to the + # running measurement + return MeasurementLoop.running_measurement.measure( + measurable, name=name, label=label, unit=unit, **kwargs + ) + + # Code from hereon is only reached by the primary measurement, + # i.e. the running_measurement + + # Wait as long as the measurement is paused + while self.is_paused: + sleep(0.1) + + t0 = perf_counter() + initial_action_indices = self.action_indices + + if timestamp: + t_now = datetime.now() + + # Store time referenced to t_start + self.measure((t_now - self._t_start).total_seconds(), + 'T_pre', unit='s', timestamp=False) + self.skip() # Increment last action index by 1 + + + + # TODO Incorporate kwargs name, label, and unit, into each of these + if isinstance(measurable, Parameter): + result = self._measure_parameter( + measurable, name=name, label=label, unit=unit, **kwargs + ) + self.skip() # Increment last action index by 1 + elif isinstance(measurable, MultiParameter): + result = self._measure_multi_parameter(measurable, name=name, **kwargs) + elif callable(measurable): + result = self._measure_callable(measurable, name=name, **kwargs) + elif isinstance(measurable, dict): + result = self._measure_dict(measurable, name=name) + elif isinstance(measurable, RAW_VALUE_TYPES): + result = self._measure_value(measurable, name=name, label=label, unit=unit, **kwargs) + self.skip() # Increment last action index by 1 + else: + raise RuntimeError( + f"Cannot measure {measurable} as it cannot be called, and it " + f"is not a dict, int, float, bool, or numpy array." + ) + + if timestamp: + t_now = datetime.now() + + # Store time referenced to t_start + self.measure((t_now - self._t_start).total_seconds(), + 'T_post', unit='s', timestamp=False) + self.skip() # Increment last action index by 1 + + + self.timings.record( + ['measurement', initial_action_indices, 'total'], + perf_counter() - t0 + ) + + return result + + # Methods related to masking of parameters/attributes/keys + def _mask_attr(self, obj: object, attr: str, value): + """Temporarily override an object attribute during the measurement. + + The value will be reset at the end of the measurement + This can also be a nested measurement. + + Args: + obj: Object whose value should be masked + attr: Attribute to be masked + val: Masked value + + Returns: + original value + """ + original_value = getattr(obj, attr) + setattr(obj, attr, value) + + self._masked_properties.append( + { + "type": "attr", + "obj": obj, + "attr": attr, + "original_value": original_value, + "value": value, + } + ) + + return original_value + + def _mask_parameter(self, param, value): + """Temporarily override a parameter value during the measurement. + + The value will be reset at the end of the measurement. + This can also be a nested measurement. + + Args: + param: Parameter whose value should be masked + val: Masked value + + Returns: + original value + """ + original_value = param() + param(value) + + self._masked_properties.append( + { + "type": "parameter", + "obj": param, + "original_value": original_value, + "value": value, + } + ) + + return original_value + + def _mask_key(self, obj: dict, key: str, value): + """Temporarily override a dictionary key during the measurement. + + The value will be reset at the end of the measurement + This can also be a nested measurement. + + Args: + obj: dictionary whose value should be masked + key: key to be masked + val: Masked value + + Returns: + original value + """ + original_value = obj[key] + obj[key] = value + + self._masked_properties.append( + { + "type": "key", + "obj": obj, + "key": key, + "original_value": original_value, + "value": value, + } + ) + + return original_value + + def mask(self, obj: Union[object, dict], val=None, **kwargs): + """Mask a key/attribute/parameter for the duration of the Measurement + + Multiple properties can be masked by passing as kwargs. + Masked properties are reverted at the end of the measurement, even if + the measurement crashes + + Args: + obj: Object from which to mask property. + For a dict, an item is masked. + For a ParameterNode, a parameter is masked. + For a parameter, the value is masked. + For all other objects, an attribute is masked. + val: Masked value, only relevant if obj is a parameter + **kwargs: Masked properties + + Returns: + List of original values before masking + + Examples: + ``` + node = ParameterNode() + node.p1 = Parameter(initial_value=1, set_cmd=None) + + with Measurement('test_masking') as msmt: + msmt.mask(node, p1=2) + print(f"node.p1 has value {node.p1}") + >>> node.p1 has value 2 + print(f"node.p1 has value {node.p1}") + >>> node.p1 has value 1 + ``` + """ + if isinstance(obj, ParameterNode): + assert val is None + # kwargs can be either parameters or attrs + return [ + self._mask_parameter(obj.parameters[key], val) + if key in obj.parameters + else self._mask_attr(obj, key, val) + for key, val in kwargs.items() + ] + if isinstance(obj, Parameter) and not kwargs: + # if kwargs are passed, they are to be treated as attrs + return self._mask_parameter(obj, val) + elif isinstance(obj, dict): + if not kwargs: + raise SyntaxError("Must pass kwargs when masking a dict") + return [self._mask_key(obj, key, val) for key, val in kwargs.items()] + else: + if not kwargs: + raise SyntaxError("Must pass kwargs when masking") + return [self._mask_attr(obj, key, val) for key, val in kwargs.items()] + + def unmask( + self, + obj, + attr=None, + key=None, + type=None, + value=None, + raise_exception=True, + **kwargs # Add kwargs because original_value may be None + ): + if 'original_value' not in kwargs: + # No masked property passed. We collect all the masked properties + # that satisfy these requirements and unmask each of them. + unmask_properties = [] + remaining_masked_properties = [] + for masked_property in self._masked_properties: + if masked_property["obj"] != obj: + remaining_masked_properties.append(masked_property) + elif attr is not None and masked_property.get("attr") != attr: + remaining_masked_properties.append(masked_property) + elif key is not None and masked_property.get("key") != key: + remaining_masked_properties.append(masked_property) + else: + unmask_properties.append(masked_property) + + for unmask_property in reversed(unmask_properties): + self.unmask(**unmask_property) + + self._masked_properties = remaining_masked_properties + else: + # A masked property has been passed, which we unmask here + try: + original_value = kwargs['original_value'] + if type == "key": + obj[key] = original_value + elif type == "attr": + setattr(obj, attr, original_value) + elif type == "parameter": + obj(original_value) + else: + raise SyntaxError(f"Unmask type {type} not understood") + except Exception as e: + self.log( + f"Could not unmask {obj} {type} from masked value {value} " + f"to original value {original_value}\n" + f"{traceback.format_exc()}", + level="error", + ) + + if raise_exception: + raise e + + def unmask_all(self): + """Unmask all masked properties""" + masked_properties = reversed(self._masked_properties) + for masked_property in masked_properties: + self.unmask(**masked_property, raise_exception=False) + self._masked_properties.clear() + + # Functions relating to measurement flow + def pause(self): + """Pause measurement at start of next parameter sweep/measurement""" + running_measurement().is_paused = True + + def resume(self): + """Resume measurement after being paused""" + running_measurement().is_paused = False + + def stop(self): + """Stop measurement at start of next parameter sweep/measurement""" + running_measurement().is_stopped = True + # Unpause loop + running_measurement().resume() + + def skip(self, N=1): + """Skip an action index. + + Useful if a measure is only sometimes run + + Args: + N: number of action indices to skip + + Examples: + This measurement repeatedly creates a random value. + It then stores the value twice, but the first time the value is + only stored if it is above a threshold. Notice that if the random + value is not above this threshold, the second measurement would + become the first measurement if msmt.skip is not called + ``` + with Measurement('skip_measurement') as msmt: + for k in Sweep(range(10)): + random_value = np.random.rand() + if random_value > 0.7: + msmt.measure(random_value, 'random_value_conditional') + else: + msmt.skip() + + msmt.measure(random_value, 'random_value_unconditional) + ``` + """ + if running_measurement() is not self: + return running_measurement().skip(N=N) + else: + action_indices = list(self.action_indices) + action_indices[-1] += N + self.action_indices = tuple(action_indices) + return self.action_indices + + def revert(self, N=1): + """Revert action indices + + Useful if you want to redo a measurement. + """ + if running_measurement() is not self: + return running_measurement().revert(N=N) + else: + action_indices = list(self.action_indices) + action_indices[-1] -= N + self.action_indices = tuple(action_indices) + return self.action_indices + + def step_out(self, reduce_dimension=True): + """Step out of a Sweep + + This function usually doesn't need to be called. + """ + if MeasurementLoop.running_measurement is not self: + MeasurementLoop.running_measurement.step_out(reduce_dimension=reduce_dimension) + else: + if reduce_dimension: + self.loop_shape = self.loop_shape[:-1] + self.loop_indices = self.loop_indices[:-1] + + # Remove last action index and increment one before that by one + action_indices = list(self.action_indices[:-1]) + action_indices[-1] += 1 + self.action_indices = tuple(action_indices) + + def traceback(self): + """Print traceback if an error occurred. + + Measurement must be ran from separate thread + """ + if self.measurement_thread is None: + raise RuntimeError('Measurement was not started in separate thread') + else: + self.measurement_thread.traceback() + +def running_measurement() -> MeasurementLoop: + """Return the running measurement""" + return MeasurementLoop.running_measurement + + +class Sweep: + """Sweep over an iterable inside a Measurement + + Args: + sequence: Sequence to iterate over. + Can be an iterable, or a parameter Sweep. + If the sequence + name: Name of sweep. Not needed if a Parameter is passed + unit: unit of sweep. Not needed if a Parameter is passed + reverse: Sweep over sequence in opposite order. + The data is also stored in reverse. + restore: Stores the state of a parameter before sweeping it, + then restores the original value upon exiting the loop. + + Examples: + ``` + with Measurement('sweep_msmt') as msmt: + for value in Sweep(np.linspace(5), 'sweep_values'): + msmt.measure(value, 'linearly_increasing_value') + + p = Parameter('my_parameter') + for param_val in Sweep(p. + ``` + """ + def __init__(self, sequence, name=None, unit=None, reverse=False, restore=False): + if running_measurement() is None: + raise RuntimeError("Cannot create a sweep outside a Measurement") + + if not isinstance(sequence, Iterable): + raise SyntaxError("Sweep sequence must be iterable") + + # Properties for the data array + self.name = name + self.unit = unit + + self.sequence = sequence + self.dimension = len(running_measurement().loop_shape) + self.loop_index = None + self.iterator = None + self.reverse = reverse + self.restore = restore + + msmt = running_measurement() + if msmt.action_indices in msmt.set_arrays: + self.set_array = msmt.set_arrays[msmt.action_indices] + else: + self.set_array = self.create_set_array() + + def __iter__(self): + if threading.current_thread() is not MeasurementLoop.measurement_thread: + raise RuntimeError( + "Cannot create a Sweep while another measurement " + "is already running in a different thread." + ) + if self.restore: + if isinstance(self.sequence, SweepValues): + running_measurement().mask(self.sequence.parameter, self.sequence.parameter.get()) + else: + raise NotImplementedError("Unable to restore non-parameter values.") + if self.reverse: + self.loop_index = len(self.sequence) - 1 + self.iterator = iter(self.sequence[::-1]) + else: + self.loop_index = 0 + self.iterator = iter(self.sequence) + + running_measurement().loop_shape += (len(self.sequence),) + running_measurement().loop_indices += (self.loop_index,) + running_measurement().action_indices += (0,) + + + return self + + def __next__(self): + msmt = running_measurement() + + if not msmt.is_context_manager: + raise RuntimeError( + "Must use the Measurement as a context manager, " + "i.e. 'with Measurement(name) as msmt:'" + ) + elif msmt.is_stopped: + raise SystemExit + + # Wait as long as the measurement is paused + while msmt.is_paused: + sleep(0.1) + + # Increment loop index of current dimension + loop_indices = list(msmt.loop_indices) + loop_indices[self.dimension] = self.loop_index + msmt.loop_indices = tuple(loop_indices) + + try: # Perform loop action + sweep_value = next(self.iterator) + # Remove last action index and increment one before that by one + action_indices = list(msmt.action_indices) + action_indices[-1] = 0 + msmt.action_indices = tuple(action_indices) + except StopIteration: # Reached end of iteration + if self.restore: + if isinstance(self.sequence, SweepValues): + msmt.unmask(self.sequence.parameter) + else: + # TODO: Check what other iterators might be able to be masked + pass + self.exit_sweep() + + if isinstance(self.sequence, SweepValues): + self.sequence.set(sweep_value) + + self.set_array[msmt.loop_indices] = sweep_value + + self.loop_index += 1 if not self.reverse else -1 + + return sweep_value + + def exit_sweep(self): + msmt = running_measurement() + msmt.step_out(reduce_dimension=True) + raise StopIteration + + def create_set_array(self): + if isinstance(self.sequence, SweepValues): + return running_measurement()._create_data_array( + action_indices=running_measurement().action_indices, + result=self.sequence, + parameter=self.sequence.parameter, + is_setpoint=True, + ) + else: + return running_measurement()._create_data_array( + action_indices=running_measurement().action_indices, + result=self.sequence, + name=self.name or "iterator", + unit=self.unit, + is_setpoint=True, + ) \ No newline at end of file diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index 8de0d5dda45..9b0db108684 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -1,3 +1,6 @@ +import builtins +import sys +import pprint import collections import io import json @@ -36,6 +39,8 @@ import numpy as np +from qcodes.configuration.config import DotDict + if TYPE_CHECKING: from PyQt5.QtWidgets import QMainWindow @@ -779,3 +784,121 @@ def checked_getattr( if not isinstance(attr, expected_type): raise TypeError() return attr + + +def using_ipython() -> bool: + """Check if code is run from IPython (including jupyter notebook/lab)""" + return hasattr(builtins, '__IPYTHON__') + + +def directly_executed_from_cell(level: int = 1) -> bool: + """Test if this function is called directly from an IPython cell + The IPython prompt is also valid. + + Args: + level: Difference in frames from IPython cell/prompt to check. + Since the check is executed from this function, the default level is 1. + + Returns: + True if directly run from IPython cell/prompt, False otherwise + + Examples: + These examples should be run in a notebook cell. + + >>> directly_executed_from_cell() + ... True + + >>> def wrap_function(**kwargs): + >>> return directly_executed_from_cell(**kwargs) + >>> wrap_function() + ... False + >>> wrap_function(level=2) + ... True + + """ + if level < 1: + raise SyntaxError('Level must be 1 or higher') + + frame = sys._getframe(level) + return '_' in frame.f_locals + + +def get_last_input_cells(cells=3): + """ + Get last input cell. Note that get_last_input_cell.globals must be set to + the ipython globals + Returns: + last cell input if successful, else None + """ + global In + if 'In' in globals() or hasattr(builtins, 'In'): + return In[-cells:] + else: + logging.warning('No input cells found') + + +def get_exponent(val): + prefactors = [(9, 'G'), (6, 'M'), (3, 'k'), (0, ''), (-3, 'm'), (-6, 'u'), (-9, 'n')] + for exponent, prefactor in prefactors: + if val >= np.power(10., exponent): + return exponent, prefactor + else: + return prefactors[-1] + + +class PerformanceTimer(): + max_records = 100 + + def __init__(self): + self.timings = DotDict() + + def __getitem__(self, key): + val = self.timings.__getitem__(key) + return self._timing_to_str(val) + + def __repr__(self): + return pprint.pformat(self._timings_to_str(self.timings), indent=2) + + def clear(self): + self.timings.clear() + + def _timing_to_str(self, val): + mean_val = np.mean(val) + exponent, prefactor = get_exponent(mean_val) + factor = np.power(10., exponent) + + return f'{mean_val / factor:.3g}+-{np.abs(np.std(val))/factor:.3g} {prefactor}s' + + def _timings_to_str(self, d: dict): + + timings_str = DotDict() + for key, val in d.items(): + if isinstance(val, dict): + timings_str[key] = self._timings_to_str(val) + else: + timings_str[key] = self._timing_to_str(val) + + return timings_str + + @contextmanager + def record(self, key, val=None): + if isinstance(key, str): + timing_list = self.timings.setdefault(key, []) + elif isinstance(key, (list)): + *parent_keys, subkey = key + d = self.timings.create_dicts(*parent_keys) + timing_list = d.setdefault(subkey, []) + else: + raise ValueError('Key must be str or list/tuple') + + if val is not None: + timing_list.append(val) + else: + t0 = time.perf_counter() + yield + t1 = time.perf_counter() + timing_list.append(t1 - t0) + + # Optionally remove oldest elements + for _ in range(len(timing_list) - self.max_records): + timing_list.pop(0) \ No newline at end of file From 89d6ed3a9210df201a9b1480caa3e9b893473aed Mon Sep 17 00:00:00 2001 From: Serwan Date: Sat, 2 Apr 2022 10:41:55 +0200 Subject: [PATCH 006/122] Begun incorporating measurement loop --- qcodes/dataset/measurement_loop.py | 73 ++++++++++++------------------ 1 file changed, 28 insertions(+), 45 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index df918c38bba..036504c684c 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -7,8 +7,7 @@ from datetime import datetime from qcodes.station import Station -from qcodes.data.data_set import new_data, DataSet -from qcodes.data.data_array import DataArray +from qcodes.instrument.base import InstrumentBase from qcodes.instrument.sweep_values import SweepValues from qcodes.instrument.parameter import Parameter, MultiParameter from qcodes.utils.helpers import ( @@ -27,12 +26,18 @@ def __init__(self, measurement_loop): self.measurement_loop = measurement_loop self.datasets = [] + def finalize(self): + """Called when outermost measurement is finished""" + def create_dataset(self): pass def new_dataset(self): pass + def add_metadata(self): + pass + def add_result(self, parameter, result, action_indices, loop_indices, loop_shape, setpoints): pass @@ -83,9 +88,9 @@ class MeasurementLoop: def __init__(self, name: str, force_cell_thread: bool = True, notify=False): self.name = name - # Dataset is created during `with Measurement('name')` - # TODO option to use multiple datasets - self.dataset = None + # Data handler is created during `with Measurement('name')` + # Used to control dataset(s) + self.data_handler = None # Total dimensionality of loop self.loop_shape: Union[Tuple[int], None] = None @@ -165,16 +170,16 @@ def __enter__(self): MeasurementLoop.running_measurement = self MeasurementLoop.measurement_thread = threading.current_thread() - # Initialize dataset + # Initialize dataset handler self.data_handler = DataHandler() - self.dataset = new_data(name=self.name) - self._initialize_metadata(self.dataset) - with self.timings.record(['dataset', 'save_metadata']): - self.dataset.save_metadata() + # TODO incorporate metadata + # self._initialize_metadata(self.dataset) + # with self.timings.record(['dataset', 'save_metadata']): + # self.dataset.save_metadata() - if hasattr(self.dataset, 'save_config'): - self.dataset.save_config() + # if hasattr(self.dataset, 'save_config'): + # self.dataset.save_config() # Initialize attributes self.loop_shape = () @@ -200,7 +205,8 @@ def __enter__(self): data_groups = [ (key, getattr(val, 'name', 'None')) for key, val in msmt.data_groups.items() ] - msmt.dataset.add_metadata({'data_groups': data_groups}) + # TODO add metadata + # msmt.dataset.add_metadata({'data_groups': data_groups}) msmt.action_indices += (0,) # Nested measurement attributes should mimic the primary measurement @@ -228,7 +234,7 @@ def __enter__(self): shell = get_ipython() shell.user_ns[self._default_measurement_name] = self - shell.user_ns[self._default_dataset_name] = self.dataset + # shell.user_ns[self._default_dataset_name] = self.dataset return self @@ -279,23 +285,11 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb): self.log("Could not notify", level="error") t_stop = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - self.dataset.add_metadata({"t_stop": t_stop}) - self.dataset.add_metadata({"timings": self.timings}) - - # If dataset only contains setpoints, don't finalize dataset. - if not all([arr.is_setpoint for arr in self.dataset.arrays.values()]): - # Sadly the timing to finalize the dataset won't be stored in the metadata. - with self.timings.record(['dataset', 'finalize']): - self.dataset.finalize() - self.dataset.active = False - else: - if hasattr(self.dataset.formatter, 'close_file'): - self.dataset.formatter.close_file(self) - self.dataset.save_metadata() + self.data_handler.add_metadata({"t_stop": t_stop}) + self.data_handler.add_metadata({"timings": self.timings}) + self.data_handler.finalize() - self.dataset.active = False - - self.log(f'Measurement finished {self.dataset.location}') + self.log(f'Measurement finished') else: msmt.step_out(reduce_dimension=False) @@ -303,11 +297,12 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb): self.is_context_manager = False def _initialize_metadata(self, dataset: DataSet = None): + # TODO Incorporate method """Initialize dataset metadata""" if dataset is None: dataset = self.dataset - config = qcodes_config.get('user', {}).get('silq_config', qcodes_config) + config = qcodes_config dataset.add_metadata({"config": config}) dataset.add_metadata({"measurement_type": "Measurement"}) @@ -337,6 +332,7 @@ def _initialize_metadata(self, dataset: DataSet = None): ) # Data array functions + # TODO Needs to be reformed def _create_data_array( self, action_indices: Tuple[int], @@ -1017,7 +1013,7 @@ def mask(self, obj: Union[object, dict], val=None, **kwargs): >>> node.p1 has value 1 ``` """ - if isinstance(obj, ParameterNode): + if isinstance(obj, InstrumentBase): assert val is None # kwargs can be either parameters or attrs return [ @@ -1146,19 +1142,6 @@ def skip(self, N=1): self.action_indices = tuple(action_indices) return self.action_indices - def revert(self, N=1): - """Revert action indices - - Useful if you want to redo a measurement. - """ - if running_measurement() is not self: - return running_measurement().revert(N=N) - else: - action_indices = list(self.action_indices) - action_indices[-1] -= N - self.action_indices = tuple(action_indices) - return self.action_indices - def step_out(self, reduce_dimension=True): """Step out of a Sweep From 4f43aed0f50bd71d0886c7009f79dd39239fbbec Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Sat, 2 Apr 2022 12:03:58 +0200 Subject: [PATCH 007/122] Added DataHandler, DatasetHandler --- qcodes/dataset/measurement_loop.py | 864 +++++++++++++++++------------ 1 file changed, 522 insertions(+), 342 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 036504c684c..bcdd3d010c1 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1,4 +1,6 @@ +from enum import unique import numpy as np +from collections import Counter from typing import List, Tuple, Union, Sequence, Dict, Any, Callable, Iterable import threading from time import sleep, perf_counter @@ -6,10 +8,12 @@ import logging from datetime import datetime +from qcodes.dataset.measurements import Measurement +from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement from qcodes.station import Station from qcodes.instrument.base import InstrumentBase from qcodes.instrument.sweep_values import SweepValues -from qcodes.instrument.parameter import Parameter, MultiParameter +from qcodes.instrument.parameter import DelegateParameter, Parameter, MultiParameter from qcodes.utils.helpers import ( using_ipython, directly_executed_from_cell, @@ -21,10 +25,191 @@ RAW_VALUE_TYPES = (float, int, bool, np.ndarray, np.integer, np.floating, np.bool_, type(None)) +class DatasetHandler: + """Handler for a single DataSet (with Measurement and Runner)""" + def __init__(self): + self.initialized = False + self.dataset = None + self.runner = None + self.measurement = None + + # Key: action_index + # Values: + # - parameter + # - dataset_parameter (differs from 'parameter' when multiple share same name) + self.setpoint_list = dict() + + self.measurement_list = dict() + # Dict with key being action_index and value is a dict containing + # - parameter + # - setpoint_parameters + # - shape + # - unstored_results - list where each element contains (*setpoints, measurement_value) + + def initialize(self): + # Once initialized, no new parameters can be added + assert not self.initialized, "Cannot initialize twice" + + self.measurement = Measurement() + + # Register all setpoints parameters + self._create_unique_dataset_parameters(self.setpoint_list) + for setpoint_info in self.setpoint_list.values(): + self.measurement.register_parameter(setpoint_info['dataset_parameter']) + + # Register all measurement parameters + self._create_unique_dataset_parameters(self.measurement_list) + for measurement_info in self.measurement_list.values(): + self.measurement.register_parameter( + measurement_info['dataset_parameter'], + setpoints=measurement_info['setpoint_parameters'] + ) + self.measurement.set_shapes( + detect_shape_of_measurement( + (measurement_info['dataset_parameter'],), + measurement_info['shape'] + ) + ) + + # Create measurement Runner + self.runner = self.measurement.run() + + # Create measurement Dataset + self.dataset = self.runner.__enter__() + + # Add results that were taken before initializing dataset + for measurement_info in self.measurement_list.values(): + for unstored_result in measurement_info['unstored_results']: + parameters = *measurement_info['setpoint_parameters'], measurement_info['dataset_parameter'] + result = tuple(zip(parameters, unstored_result)) + self.dataset.add_result(result) + + self.initialized = True + + def _create_unique_dataset_parameters(self, parameter_list): + """Populates 'dataset_parameter' of parameter_list + + Ensure parameters have unique names + """ + parameter_names = [param_info['parameter'].name for param_info in parameter_list] + duplicate_names = [name for name, count in Counter(parameter_names) if count > 1] + unique_names = [name for name, count in Counter(parameter_names) if count == 1] + + for name in unique_names: + parameter_info = next( + param_info for param_info in parameter_list + if param_info['parameter'].name == name + ) + parameter_info['dataset_parameter'] = parameter_info['parameter'] + + for name in duplicate_names: + # Need to rename parameters with duplicate names + duplicate_parameter_info_list = [ + param_info for param_info in parameter_list + if param_info['parameter'].name == name + ] + for k, parameter_info in duplicate_parameter_info_list: + # Create delegate parameter + delegate_parameter = DelegateParameter( + name=f"{parameter_info['parameter'].name}_{k}", + source=parameter_info['parameter'] + ) + parameter_info['dataset_parameter'] = delegate_parameter + + + def add_measurement_result( + self, + action_indices, + result, + parameter=None, + name: str = None, + label: str = None, + unit: str = None, + ): + """Store single measurement result + + This method is called from type-specific methods, such as + ``_measure_value``, ``_measure_parameter``, etc. + """ + if parameter is None and name is None: + raise SyntaxError( + "When adding a measurement result, must provide either a " + "parameter or name" + ) + + # Get parameter data array, creating a new one if necessary + # TODO Need to handle when a parameter is not being passed + if action_indices not in self.measurement_list: + assert not self.initialized, "Cannot measure parameter for the first time after initializing dataset" + + self.measurement_list[action_indices] = { + 'parameter': parameter, + 'setpoint_parameters': None, # TODO + 'shape': None, # TODO + 'unstored_results': [] + } + + # Select existing array + data_array = self.data_arrays[action_indices] + + # Ensure an existing data array has the correct name + # parameter can also be a string, in which case we don't use parameter.name + if name is None: + name = parameter.name + + # TODO is this the right place for this check? + if not data_array.name == name: + raise SyntaxError( + f"Existing DataArray '{data_array.name}' differs from result {name}" + ) + + data_to_store = {data_array.array_id: result} + + # If result is an array, update set_array elements + if isinstance(result, list): # Convert result list to array + result = np.ndarray(result) + if isinstance(result, np.ndarray): + ndim = len(self.loop_indices) + if len(data_array.set_arrays) != ndim + result.ndim: + raise RuntimeError( + f"Wrong number of set arrays for {data_array.name}. " + f"Expected {ndim + result.ndim} instead of " + f"{len(data_array.set_arrays)}." + ) + + for k, set_array in enumerate(data_array.set_arrays[ndim:]): + # Successive set arrays must increase dimensionality by unity + arr = np.arange(result.shape[k]) + if parameter is not None and hasattr(parameter, 'setpoints') \ + and parameter.setpoints is not None: + arr_idx = parameter.names.index(name) + arr = parameter.setpoints[arr_idx][k] + + # Add singleton dimensions + arr = np.broadcast_to(arr, result.shape[: k + 1]) + data_to_store[set_array.array_id] = arr + + # Use dummy index if there are no loop indices. + # This happens if the measurement is performed outside a Sweep + loop_indices = self.loop_indices + if not loop_indices and not isinstance(result, (list, np.ndarray)): + loop_indices = (0,) + + return data_to_store + + class DataHandler: def __init__(self, measurement_loop): + # MeasurementLoop corresponding to this DataHandler + # Cannot be a nested MeasurementLoop self.measurement_loop = measurement_loop - self.datasets = [] + + self.dataset_handlers = [] + + @property + def active_dataset_handler(self): + # TODO Allow for multiple possible measurements + return self.measurements[0] def finalize(self): """Called when outermost measurement is finished""" @@ -38,8 +223,266 @@ def new_dataset(self): def add_metadata(self): pass - def add_result(self, parameter, result, action_indices, loop_indices, loop_shape, setpoints): - pass + def add_measurement_result( + self, + action_indices, + result, + parameter=None, + name: str = None, + label: str = None, + unit: str = None, + ): + """Store single measurement result + + This method is called from type-specific methods, such as + ``_measure_value``, ``_measure_parameter``, etc. + """ + if parameter is None and name is None: + raise SyntaxError( + "When adding a measurement result, must provide either a " + "parameter or name" + ) + + # Get parameter data array, creating a new one if necessary + if action_indices not in self.data_arrays: + # Create array based on first result type and shape + self._create_data_array( + action_indices, + result, + parameter=parameter, + name=name, + label=label, + unit=unit, + ) + + # Select existing array + data_array = self.data_arrays[action_indices] + + # Ensure an existing data array has the correct name + # parameter can also be a string, in which case we don't use parameter.name + if name is None: + name = parameter.name + + # TODO is this the right place for this check? + if not data_array.name == name: + raise SyntaxError( + f"Existing DataArray '{data_array.name}' differs from result {name}" + ) + + data_to_store = {data_array.array_id: result} + + # If result is an array, update set_array elements + if isinstance(result, list): # Convert result list to array + result = np.ndarray(result) + if isinstance(result, np.ndarray): + ndim = len(self.loop_indices) + if len(data_array.set_arrays) != ndim + result.ndim: + raise RuntimeError( + f"Wrong number of set arrays for {data_array.name}. " + f"Expected {ndim + result.ndim} instead of " + f"{len(data_array.set_arrays)}." + ) + + for k, set_array in enumerate(data_array.set_arrays[ndim:]): + # Successive set arrays must increase dimensionality by unity + arr = np.arange(result.shape[k]) + if parameter is not None and hasattr(parameter, 'setpoints') \ + and parameter.setpoints is not None: + arr_idx = parameter.names.index(name) + arr = parameter.setpoints[arr_idx][k] + + # Add singleton dimensions + arr = np.broadcast_to(arr, result.shape[: k + 1]) + data_to_store[set_array.array_id] = arr + + # Use dummy index if there are no loop indices. + # This happens if the measurement is performed outside a Sweep + loop_indices = self.loop_indices + if not loop_indices and not isinstance(result, (list, np.ndarray)): + loop_indices = (0,) + + return data_to_store + + # Data array functions + # TODO Needs to be reformed + def _create_data_array( + self, + action_indices: Tuple[int], + result, + parameter: Parameter = None, + is_setpoint: bool = False, + name: str = None, + label: str = None, + unit: str = None, + ): + """Create a data array from a parameter and result. + + The data array shape is extracted from the result shape, and the current + loop dimensions. + + The data array is added to the current data set. + + Args: + parameter: Parameter for which to create a DataArray. Can also be a + string, in which case it is the data_array name + result: Result returned by the Parameter + action_indices: Action indices for which to store parameter + is_setpoint: Whether the Parameter is used for sweeping or measuring + label: Data array label. If not provided, the parameter label is + used. If the parameter is a name string, the label is extracted + from the name. + unit: Data array unit. If not provided, the parameter unit is used. + + Returns: + Newly created data array + + """ + if parameter is None and name is None: + raise SyntaxError( + "When creating a data array, must provide either a parameter or a name" + ) + + if len(running_measurement().data_arrays) >= self.max_arrays: + raise RuntimeError( + f"Number of arrays in dataset exceeds " + f"Measurement.max_arrays={self.max_arrays}. Perhaps you forgot" + f"to encapsulate a loop with a Sweep()?" + ) + + array_kwargs = { + "is_setpoint": is_setpoint, + "action_indices": action_indices, + "shape": self.loop_shape, + } + + if is_setpoint or isinstance(result, (np.ndarray, list)): + array_kwargs["shape"] += np.shape(result) + + # Use dummy index (1, ) if measurement is performed outside a Sweep + if not array_kwargs["shape"]: + array_kwargs["shape"] = (1,) + + if isinstance(parameter, Parameter): + array_kwargs["parameter"] = parameter + # Add a custom name + if name is not None: + array_kwargs["full_name"] = name + if label is not None: + array_kwargs["label"] = label + if unit is not None: + array_kwargs["unit"] = unit + else: + array_kwargs["name"] = name + if label is None: + label = name[0].capitalize() + name[1:].replace("_", " ") + array_kwargs["label"] = label + array_kwargs["unit"] = unit or "" + + # Add setpoint arrays + if not is_setpoint: + array_kwargs["set_arrays"] = self._add_set_arrays( + action_indices, result, parameter=parameter, name=(name or parameter.name) + ) + + data_array = DataArray(**array_kwargs) + + data_array.array_id = data_array.full_name + data_array.array_id += "_" + "_".join(str(k) for k in action_indices) + + data_array.init_data() + + self.dataset.add_array(data_array) + with self.timings.record(['dataset', 'save_metadata']): + self.dataset.save_metadata() + + # Add array to set_arrays or to data_arrays of this Measurement + if is_setpoint: + self.set_arrays[action_indices] = data_array + else: + self.data_arrays[action_indices] = data_array + + return data_array + + def _add_set_arrays( + self, action_indices: Tuple[int], result, name: str, parameter: Union[Parameter, None] = None + ): + """Create set arrays for a given action index""" + set_arrays = [] + for k in range(1, len(action_indices)): + sweep_indices = action_indices[:k] + + if sweep_indices in self.set_arrays: + set_arrays.append(self.set_arrays[sweep_indices]) + # TODO handle grouped arrays (e.g. ParameterNode, nested Measurement) + # Create new set array(s) if parameter result is an array or list + if isinstance(result, (np.ndarray, list)): + if isinstance(result, list): + result = np.ndarray(result) + + for k, shape in enumerate(result.shape): + arr = np.arange(shape) + label = None + unit = None + if parameter is not None and hasattr(parameter, 'setpoints') \ + and parameter.setpoints is not None: + arr_idx = parameter.names.index(name) + arr = parameter.setpoints[arr_idx][k] + label = parameter.setpoint_labels[arr_idx][k] + unit = parameter.setpoint_units[arr_idx][k] + + # Add singleton dimensions + arr = np.broadcast_to(arr, result.shape[: k + 1]) + + set_array = self._create_data_array( + action_indices=action_indices + (0,) * k, + result=arr, + name=f"{name}_set{k}", + label=label, + unit=unit, + is_setpoint=True, + ) + set_arrays.append(set_array) + + # Add a dummy array in case the measurement was performed outside of + # a Sweep. This is not needed if the result is an array + if not set_arrays and not self.loop_indices: + set_arrays = [ + self._create_data_array( + action_indices=running_measurement().action_indices, + result=result, + name="None", + is_setpoint=True, + ) + ] + set_arrays[0][0] = 1 + + return tuple(set_arrays) + + def get_arrays(self, action_indices: Sequence[int] = None) -> List[DataArray]: + """Get all arrays belonging to the current action indices + + If the action indices corresponds to a group of arrays (e.g. a nested + measurement or ParameterNode), all the arrays in the group are returned + + Args: + action_indices: Action indices of arrays. + If not provided, the current action_indices are chosen + + Returns: + List of data arrays matching the action indices + """ + if action_indices is None: + action_indices = self.action_indices + + if not isinstance(action_indices, Sequence): + raise SyntaxError("parent_action_indices must be a tuple") + + num_indices = len(action_indices) + return [ + arr + for action_indices, arr in self.data_arrays.items() + if action_indices[:num_indices] == action_indices + ] class MeasurementLoop: @@ -244,273 +687,92 @@ def __enter__(self): MeasurementLoop.running_measurement = None raise - def __exit__(self, exc_type: Exception, exc_val, exc_tb): - """Operation when exiting a loop - - Args: - exc_type: Type of exception, None if no exception - exc_val: Exception message, None if no exception - exc_tb: Exception traceback object, None if no exception - """ - msmt = MeasurementLoop.running_measurement - if msmt is self: - # Immediately unregister measurement as main measurement, in case - # an error occurs during final actions. - MeasurementLoop.running_measurement = None - - if exc_type is not None: - self.log(f"Measurement error {exc_type.__name__}({exc_val})", level="error") - - self._apply_actions(self.except_actions, label="except", clear=True) - - if msmt is self: - self._apply_actions( - MeasurementLoop.except_actions, label="global except", clear=True - ) - - self._apply_actions(self.final_actions, label="final", clear=True) - - self.unmask_all() - - if msmt is self: - # Also perform global final actions - # These are always performed when outermost measurement finishes - self._apply_actions(MeasurementLoop.final_actions, label="global final") - - # Notify that measurement is complete - if self.notify and self.notify_function is not None: - try: - self.notify_function(exc_type, exc_val, exc_tb) - except: - self.log("Could not notify", level="error") - - t_stop = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - self.data_handler.add_metadata({"t_stop": t_stop}) - self.data_handler.add_metadata({"timings": self.timings}) - self.data_handler.finalize() - - self.log(f'Measurement finished') - - else: - msmt.step_out(reduce_dimension=False) - - self.is_context_manager = False - - def _initialize_metadata(self, dataset: DataSet = None): - # TODO Incorporate method - """Initialize dataset metadata""" - if dataset is None: - dataset = self.dataset - - config = qcodes_config - dataset.add_metadata({"config": config}) - - dataset.add_metadata({"measurement_type": "Measurement"}) - - # Add instrument information - if Station.default is not None: - dataset.add_metadata({"station": Station.default.snapshot()}) - - if using_ipython(): - measurement_cell = get_last_input_cells(1)[0] - - measurement_code = measurement_cell - # If the code is run from a measurement thread, there is some - # initial code that should be stripped - init_string = "get_ipython().run_cell_magic('new_job', '', " - if measurement_code.startswith(init_string): - measurement_code = measurement_code[len(init_string) + 1 : -4] - - self._t_start = datetime.now() - dataset.add_metadata( - { - "measurement_cell": measurement_cell, - "measurement_code": measurement_code, - "last_input_cells": get_last_input_cells(20), - "t_start": self._t_start.strftime('%Y-%m-%d %H:%M:%S') - } - ) - - # Data array functions - # TODO Needs to be reformed - def _create_data_array( - self, - action_indices: Tuple[int], - result, - parameter: Parameter = None, - is_setpoint: bool = False, - name: str = None, - label: str = None, - unit: str = None, - ): - """Create a data array from a parameter and result. - - The data array shape is extracted from the result shape, and the current - loop dimensions. - - The data array is added to the current data set. - - Args: - parameter: Parameter for which to create a DataArray. Can also be a - string, in which case it is the data_array name - result: Result returned by the Parameter - action_indices: Action indices for which to store parameter - is_setpoint: Whether the Parameter is used for sweeping or measuring - label: Data array label. If not provided, the parameter label is - used. If the parameter is a name string, the label is extracted - from the name. - unit: Data array unit. If not provided, the parameter unit is used. - - Returns: - Newly created data array - - """ - if parameter is None and name is None: - raise SyntaxError( - "When creating a data array, must provide either a parameter or a name" - ) - - if len(running_measurement().data_arrays) >= self.max_arrays: - raise RuntimeError( - f"Number of arrays in dataset exceeds " - f"Measurement.max_arrays={self.max_arrays}. Perhaps you forgot" - f"to encapsulate a loop with a Sweep()?" - ) - - array_kwargs = { - "is_setpoint": is_setpoint, - "action_indices": action_indices, - "shape": self.loop_shape, - } - - if is_setpoint or isinstance(result, (np.ndarray, list)): - array_kwargs["shape"] += np.shape(result) + def __exit__(self, exc_type: Exception, exc_val, exc_tb): + """Operation when exiting a loop - # Use dummy index (1, ) if measurement is performed outside a Sweep - if not array_kwargs["shape"]: - array_kwargs["shape"] = (1,) + Args: + exc_type: Type of exception, None if no exception + exc_val: Exception message, None if no exception + exc_tb: Exception traceback object, None if no exception + """ + msmt = MeasurementLoop.running_measurement + if msmt is self: + # Immediately unregister measurement as main measurement, in case + # an error occurs during final actions. + MeasurementLoop.running_measurement = None - if isinstance(parameter, Parameter): - array_kwargs["parameter"] = parameter - # Add a custom name - if name is not None: - array_kwargs["full_name"] = name - if label is not None: - array_kwargs["label"] = label - if unit is not None: - array_kwargs["unit"] = unit - else: - array_kwargs["name"] = name - if label is None: - label = name[0].capitalize() + name[1:].replace("_", " ") - array_kwargs["label"] = label - array_kwargs["unit"] = unit or "" + if exc_type is not None: + self.log(f"Measurement error {exc_type.__name__}({exc_val})", level="error") - # Add setpoint arrays - if not is_setpoint: - array_kwargs["set_arrays"] = self._add_set_arrays( - action_indices, result, parameter=parameter, name=(name or parameter.name) - ) + self._apply_actions(self.except_actions, label="except", clear=True) - data_array = DataArray(**array_kwargs) + if msmt is self: + self._apply_actions( + MeasurementLoop.except_actions, label="global except", clear=True + ) - data_array.array_id = data_array.full_name - data_array.array_id += "_" + "_".join(str(k) for k in action_indices) + self._apply_actions(self.final_actions, label="final", clear=True) - data_array.init_data() + self.unmask_all() - self.dataset.add_array(data_array) - with self.timings.record(['dataset', 'save_metadata']): - self.dataset.save_metadata() + if msmt is self: + # Also perform global final actions + # These are always performed when outermost measurement finishes + self._apply_actions(MeasurementLoop.final_actions, label="global final") - # Add array to set_arrays or to data_arrays of this Measurement - if is_setpoint: - self.set_arrays[action_indices] = data_array - else: - self.data_arrays[action_indices] = data_array + # Notify that measurement is complete + if self.notify and self.notify_function is not None: + try: + self.notify_function(exc_type, exc_val, exc_tb) + except: + self.log("Could not notify", level="error") - return data_array + t_stop = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + self.data_handler.add_metadata({"t_stop": t_stop}) + self.data_handler.add_metadata({"timings": self.timings}) + self.data_handler.finalize() - def _add_set_arrays( - self, action_indices: Tuple[int], result, name: str, parameter: Union[Parameter, None] = None - ): - """Create set arrays for a given action index""" - set_arrays = [] - for k in range(1, len(action_indices)): - sweep_indices = action_indices[:k] - - if sweep_indices in self.set_arrays: - set_arrays.append(self.set_arrays[sweep_indices]) - # TODO handle grouped arrays (e.g. ParameterNode, nested Measurement) - # Create new set array(s) if parameter result is an array or list - if isinstance(result, (np.ndarray, list)): - if isinstance(result, list): - result = np.ndarray(result) - - for k, shape in enumerate(result.shape): - arr = np.arange(shape) - label = None - unit = None - if parameter is not None and hasattr(parameter, 'setpoints') \ - and parameter.setpoints is not None: - arr_idx = parameter.names.index(name) - arr = parameter.setpoints[arr_idx][k] - label = parameter.setpoint_labels[arr_idx][k] - unit = parameter.setpoint_units[arr_idx][k] - - # Add singleton dimensions - arr = np.broadcast_to(arr, result.shape[: k + 1]) + self.log(f'Measurement finished') - set_array = self._create_data_array( - action_indices=action_indices + (0,) * k, - result=arr, - name=f"{name}_set{k}", - label=label, - unit=unit, - is_setpoint=True, - ) - set_arrays.append(set_array) + else: + msmt.step_out(reduce_dimension=False) - # Add a dummy array in case the measurement was performed outside of - # a Sweep. This is not needed if the result is an array - if not set_arrays and not self.loop_indices: - set_arrays = [ - self._create_data_array( - action_indices=running_measurement().action_indices, - result=result, - name="None", - is_setpoint=True, - ) - ] - set_arrays[0][0] = 1 + self.is_context_manager = False - return tuple(set_arrays) + # TODO Needs to be implemented + def _initialize_metadata(self, dataset): + """Initialize dataset metadata""" + if dataset is None: + dataset = self.dataset - def get_arrays(self, action_indices: Sequence[int] = None) -> List[DataArray]: - """Get all arrays belonging to the current action indices + config = qcodes_config + dataset.add_metadata({"config": config}) - If the action indices corresponds to a group of arrays (e.g. a nested - measurement or ParameterNode), all the arrays in the group are returned + dataset.add_metadata({"measurement_type": "Measurement"}) - Args: - action_indices: Action indices of arrays. - If not provided, the current action_indices are chosen + # Add instrument information + if Station.default is not None: + dataset.add_metadata({"station": Station.default.snapshot()}) - Returns: - List of data arrays matching the action indices - """ - if action_indices is None: - action_indices = self.action_indices + if using_ipython(): + measurement_cell = get_last_input_cells(1)[0] - if not isinstance(action_indices, Sequence): - raise SyntaxError("parent_action_indices must be a tuple") + measurement_code = measurement_cell + # If the code is run from a measurement thread, there is some + # initial code that should be stripped + init_string = "get_ipython().run_cell_magic('new_job', '', " + if measurement_code.startswith(init_string): + measurement_code = measurement_code[len(init_string) + 1 : -4] - num_indices = len(action_indices) - return [ - arr - for action_indices, arr in self.data_arrays.items() - if action_indices[:num_indices] == action_indices - ] + self._t_start = datetime.now() + dataset.add_metadata( + { + "measurement_cell": measurement_cell, + "measurement_code": measurement_code, + "last_input_cells": get_last_input_cells(20), + "t_start": self._t_start.strftime('%Y-%m-%d %H:%M:%S') + } + ) def _verify_action(self, action, name, add_if_new=True): """Verify an action corresponds to the current action indices. @@ -529,91 +791,6 @@ def _verify_action(self, action, name, add_if_new=True): f"Expected: {self.action_names[self.action_indices]}. Received: {name}" ) - def _add_measurement_result( - self, - action_indices, - result, - parameter=None, - store: bool = True, - name: str = None, - label: str = None, - unit: str = None, - ): - """Store single measurement result - - This method is called from type-specific methods, such as - ``_measure_value``, ``_measure_parameter``, etc. - """ - if parameter is None and name is None: - raise SyntaxError( - "When adding a measurement result, must provide either a " - "parameter or name" - ) - - # Get parameter data array, creating a new one if necessary - if action_indices not in self.data_arrays: - # Create array based on first result type and shape - self._create_data_array( - action_indices, - result, - parameter=parameter, - name=name, - label=label, - unit=unit, - ) - - # Select existing array - data_array = self.data_arrays[action_indices] - - # Ensure an existing data array has the correct name - # parameter can also be a string, in which case we don't use parameter.name - if name is None: - name = parameter.name - - # TODO is this the right place for this check? - if not data_array.name == name: - raise SyntaxError( - f"Existing DataArray '{data_array.name}' differs from result {name}" - ) - - data_to_store = {data_array.array_id: result} - - # If result is an array, update set_array elements - if isinstance(result, list): # Convert result list to array - result = np.ndarray(result) - if isinstance(result, np.ndarray): - ndim = len(self.loop_indices) - if len(data_array.set_arrays) != ndim + result.ndim: - raise RuntimeError( - f"Wrong number of set arrays for {data_array.name}. " - f"Expected {ndim + result.ndim} instead of " - f"{len(data_array.set_arrays)}." - ) - - for k, set_array in enumerate(data_array.set_arrays[ndim:]): - # Successive set arrays must increase dimensionality by unity - arr = np.arange(result.shape[k]) - if parameter is not None and hasattr(parameter, 'setpoints') \ - and parameter.setpoints is not None: - arr_idx = parameter.names.index(name) - arr = parameter.setpoints[arr_idx][k] - - # Add singleton dimensions - arr = np.broadcast_to(arr, result.shape[: k + 1]) - data_to_store[set_array.array_id] = arr - - # Use dummy index if there are no loop indices. - # This happens if the measurement is performed outside a Sweep - loop_indices = self.loop_indices - if not loop_indices and not isinstance(result, (list, np.ndarray)): - loop_indices = (0,) - - if store: - with self.timings.record(['dataset', 'store']): - self.dataset.store(loop_indices, data_to_store) - - return data_to_store - def _apply_actions(self, actions: list, label="", clear=False): """Apply actions, either except_actions or final_actions""" for action in actions: @@ -630,6 +807,7 @@ def _apply_actions(self, actions: list, label="", clear=False): actions.clear() # Measurement-related functions + # TODO these methods should always end up with a parameter def _measure_parameter(self, parameter, name=None, label=None, unit=None, **kwargs): """Measure parameter and store results. @@ -644,9 +822,9 @@ def _measure_parameter(self, parameter, name=None, label=None, unit=None, **kwar # Get parameter result result = parameter(**kwargs) - self._add_measurement_result( - self.action_indices, - result, + self.data_handler.add_measurement_result( + action_indices=self.action_indices, + result=result, parameter=parameter, name=name, label=label, @@ -698,7 +876,7 @@ def _measure_callable(self, callable, name=None, **kwargs): # Determine name if name is None: if hasattr(callable, "__self__") and isinstance( - callable.__self__, ParameterNode + callable.__self__, InstrumentBase ): name = callable.__self__.name elif hasattr(callable, "__name__"): @@ -772,7 +950,7 @@ def _measure_value(self, value, name, parameter=None, label=None, unit=None): value = bool(value) result = value - self._add_measurement_result( + self.data_handler.add_measurement_result( action_indices=self.action_indices, result=result, parameter=parameter, @@ -1169,11 +1347,13 @@ def traceback(self): else: self.measurement_thread.traceback() + def running_measurement() -> MeasurementLoop: """Return the running measurement""" return MeasurementLoop.running_measurement +# TODO Any mention of set array should be changed class Sweep: """Sweep over an iterable inside a Measurement From 60a1d8c8b3bb358537806a5b476d1ab56da142d3 Mon Sep 17 00:00:00 2001 From: Serwan Date: Sat, 2 Apr 2022 19:15:44 +0200 Subject: [PATCH 008/122] Fixed Sweep --- qcodes/dataset/measurement_loop.py | 136 ++++++++++++++--------------- 1 file changed, 66 insertions(+), 70 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index bcdd3d010c1..ab11e2d6734 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -10,6 +10,7 @@ from qcodes.dataset.measurements import Measurement from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement +from qcodes.utils.dataset.doNd import AbstractSweep from qcodes.station import Station from qcodes.instrument.base import InstrumentBase from qcodes.instrument.sweep_values import SweepValues @@ -37,6 +38,7 @@ def __init__(self): # Values: # - parameter # - dataset_parameter (differs from 'parameter' when multiple share same name) + # - latest_value # TODO self.setpoint_list = dict() self.measurement_list = dict() @@ -45,6 +47,7 @@ def __init__(self): # - setpoint_parameters # - shape # - unstored_results - list where each element contains (*setpoints, measurement_value) + # - latest_value # TODO def initialize(self): # Once initialized, no new parameters can be added @@ -137,8 +140,11 @@ def add_measurement_result( "parameter or name" ) + assert parameter is not None + # Get parameter data array, creating a new one if necessary # TODO Need to handle when a parameter is not being passed + # TODO Make sure name, label, unit would overwrite parameter if action_indices not in self.measurement_list: assert not self.initialized, "Cannot measure parameter for the first time after initializing dataset" @@ -149,53 +155,22 @@ def add_measurement_result( 'unstored_results': [] } - # Select existing array - data_array = self.data_arrays[action_indices] + measurement_info = self.measurement_list[action_indices] - # Ensure an existing data array has the correct name - # parameter can also be a string, in which case we don't use parameter.name - if name is None: - name = parameter.name + # TODO add check that parameter (or name) matches that of measurement_info - # TODO is this the right place for this check? - if not data_array.name == name: - raise SyntaxError( - f"Existing DataArray '{data_array.name}' differs from result {name}" + # Store result + setpoints = None # TODO + result_with_setpoints = tuple(zip(parameters, (*setpoints, result))) + if self.initialized: + parameters = ( + *measurement_info['setpoint_parameters'], + measurement_info['dataset_parameter'] ) - - data_to_store = {data_array.array_id: result} - - # If result is an array, update set_array elements - if isinstance(result, list): # Convert result list to array - result = np.ndarray(result) - if isinstance(result, np.ndarray): - ndim = len(self.loop_indices) - if len(data_array.set_arrays) != ndim + result.ndim: - raise RuntimeError( - f"Wrong number of set arrays for {data_array.name}. " - f"Expected {ndim + result.ndim} instead of " - f"{len(data_array.set_arrays)}." - ) - - for k, set_array in enumerate(data_array.set_arrays[ndim:]): - # Successive set arrays must increase dimensionality by unity - arr = np.arange(result.shape[k]) - if parameter is not None and hasattr(parameter, 'setpoints') \ - and parameter.setpoints is not None: - arr_idx = parameter.names.index(name) - arr = parameter.setpoints[arr_idx][k] - - # Add singleton dimensions - arr = np.broadcast_to(arr, result.shape[: k + 1]) - data_to_store[set_array.array_id] = arr - - # Use dummy index if there are no loop indices. - # This happens if the measurement is performed outside a Sweep - loop_indices = self.loop_indices - if not loop_indices and not isinstance(result, (list, np.ndarray)): - loop_indices = (0,) - - return data_to_store + + self.dataset.add_result(result_with_setpoints) + else: + measurement_info['unstored_results'].append(result_with_setpoints) class DataHandler: @@ -601,6 +576,20 @@ def active_action(self): def active_action_name(self): return self.action_names.get(self.action_indices, None) + @property + def setpoint_list(self): + if self.data_handler is not None: + return self.data_handler.setpoint_list + else: + return None + + @property + def measurement_list(self): + if self.data_handler is not None: + return self.data_handler.measurement_list + else: + return None + def __enter__(self): """Operation when entering a loop""" self.is_context_manager = True @@ -1353,7 +1342,6 @@ def running_measurement() -> MeasurementLoop: return MeasurementLoop.running_measurement -# TODO Any mention of set array should be changed class Sweep: """Sweep over an iterable inside a Measurement @@ -1378,7 +1366,7 @@ class Sweep: for param_val in Sweep(p. ``` """ - def __init__(self, sequence, name=None, unit=None, reverse=False, restore=False): + def __init__(self, sequence, name=None, label=None, unit=None, reverse=False, restore=False): if running_measurement() is None: raise RuntimeError("Cannot create a sweep outside a Measurement") @@ -1387,6 +1375,7 @@ def __init__(self, sequence, name=None, unit=None, reverse=False, restore=False) # Properties for the data array self.name = name + self.label = label self.unit = unit self.sequence = sequence @@ -1396,11 +1385,10 @@ def __init__(self, sequence, name=None, unit=None, reverse=False, restore=False) self.reverse = reverse self.restore = restore + # Create setpoint_list + self.initialize() msmt = running_measurement() - if msmt.action_indices in msmt.set_arrays: - self.set_array = msmt.set_arrays[msmt.action_indices] - else: - self.set_array = self.create_set_array() + self.setpoint_info = msmt.setpoint_list[msmt.action_indices] def __iter__(self): if threading.current_thread() is not MeasurementLoop.measurement_thread: @@ -1424,7 +1412,6 @@ def __iter__(self): running_measurement().loop_indices += (self.loop_index,) running_measurement().action_indices += (0,) - return self def __next__(self): @@ -1465,30 +1452,39 @@ def __next__(self): if isinstance(self.sequence, SweepValues): self.sequence.set(sweep_value) - self.set_array[msmt.loop_indices] = sweep_value + self.setpoint_info['latest_value'] = sweep_value self.loop_index += 1 if not self.reverse else -1 return sweep_value - def exit_sweep(self): + def initialize(self): msmt = running_measurement() - msmt.step_out(reduce_dimension=True) - raise StopIteration + assert msmt.action_indices not in msmt.setpoint_list, f"Setpoint {self.name} already initialized" - def create_set_array(self): - if isinstance(self.sequence, SweepValues): - return running_measurement()._create_data_array( - action_indices=running_measurement().action_indices, - result=self.sequence, - parameter=self.sequence.parameter, - is_setpoint=True, - ) + # Determine sweep parameter + if isinstance(self.sequence, AbstractSweep) and hasattr(self.sequence, '_param'): + # sweep is a doNd sweep that already has a parameter + set_parameter = self.sequence._param else: - return running_measurement()._create_data_array( - action_indices=running_measurement().action_indices, - result=self.sequence, - name=self.name or "iterator", - unit=self.unit, - is_setpoint=True, - ) \ No newline at end of file + # Need to create a parameter + set_parameter = Parameter( + name=self.name, + label=self.label, + unit=self.unit + ) + + setpoint_info = { + 'parameter': set_parameter, + 'latest_value': None + } + + # Add to setpoint list + msmt.setpoint_list[msmt.action_indices] = setpoint_info + + return setpoint_info + + def exit_sweep(self): + msmt = running_measurement() + msmt.step_out(reduce_dimension=True) + raise StopIteration \ No newline at end of file From 0f1cab0ed2a5af61c1354e8237d277a795d5ebda Mon Sep 17 00:00:00 2001 From: Serwan Date: Sat, 2 Apr 2022 20:44:26 +0200 Subject: [PATCH 009/122] Gone through entire file, but needs testing --- qcodes/dataset/measurement_loop.py | 377 ++++++----------------------- 1 file changed, 70 insertions(+), 307 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index ab11e2d6734..e89aff90068 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -10,6 +10,7 @@ from qcodes.dataset.measurements import Measurement from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement +from qcodes.tests.dataset import measurement from qcodes.utils.dataset.doNd import AbstractSweep from qcodes.station import Station from qcodes.instrument.base import InstrumentBase @@ -21,14 +22,16 @@ get_last_input_cells, PerformanceTimer ) -from qcodes import config as qcodes_config +from qcodes import config as qcodes_config, measure RAW_VALUE_TYPES = (float, int, bool, np.ndarray, np.integer, np.floating, np.bool_, type(None)) class DatasetHandler: """Handler for a single DataSet (with Measurement and Runner)""" - def __init__(self): + def __init__(self, measurement_loop): + self.measurement_loop = measurement_loop + self.initialized = False self.dataset = None self.runner = None @@ -38,16 +41,17 @@ def __init__(self): # Values: # - parameter # - dataset_parameter (differs from 'parameter' when multiple share same name) - # - latest_value # TODO + # - latest_value self.setpoint_list = dict() self.measurement_list = dict() # Dict with key being action_index and value is a dict containing # - parameter + # - setpoints_action_indices # - setpoint_parameters # - shape # - unstored_results - list where each element contains (*setpoints, measurement_value) - # - latest_value # TODO + # - latest_value def initialize(self): # Once initialized, no new parameters can be added @@ -119,84 +123,48 @@ def _create_unique_dataset_parameters(self, parameter_list): ) parameter_info['dataset_parameter'] = delegate_parameter - - def add_measurement_result( + def create_measurement_info( self, action_indices, - result, - parameter=None, - name: str = None, - label: str = None, - unit: str = None, - ): - """Store single measurement result - - This method is called from type-specific methods, such as - ``_measure_value``, ``_measure_parameter``, etc. - """ - if parameter is None and name is None: - raise SyntaxError( - "When adding a measurement result, must provide either a " - "parameter or name" - ) - - assert parameter is not None - - # Get parameter data array, creating a new one if necessary - # TODO Need to handle when a parameter is not being passed - # TODO Make sure name, label, unit would overwrite parameter - if action_indices not in self.measurement_list: - assert not self.initialized, "Cannot measure parameter for the first time after initializing dataset" - - self.measurement_list[action_indices] = { - 'parameter': parameter, - 'setpoint_parameters': None, # TODO - 'shape': None, # TODO - 'unstored_results': [] + parameter, + name=None, + label=None, + unit=None + ): + assert not self.initialized, "Cannot measure parameter for the first time after initializing dataset" + + if parameter is None: + assert name is not None + parameter = Parameter(name=name, label=label, unit=unit) + elif {name, label, unit} != {None, }: + overwrite_attrs = { + 'name': name, + 'label': label, + 'unit': unit } - - measurement_info = self.measurement_list[action_indices] - - # TODO add check that parameter (or name) matches that of measurement_info - - # Store result - setpoints = None # TODO - result_with_setpoints = tuple(zip(parameters, (*setpoints, result))) - if self.initialized: - parameters = ( - *measurement_info['setpoint_parameters'], - measurement_info['dataset_parameter'] + overwrite_attrs = {key: val for key, val in overwrite_attrs if val is not None} + parameter = DelegateParameter( + source=parameter, + **overwrite_attrs ) - - self.dataset.add_result(result_with_setpoints) - else: - measurement_info['unstored_results'].append(result_with_setpoints) - -class DataHandler: - def __init__(self, measurement_loop): - # MeasurementLoop corresponding to this DataHandler - # Cannot be a nested MeasurementLoop - self.measurement_loop = measurement_loop - - self.dataset_handlers = [] - - @property - def active_dataset_handler(self): - # TODO Allow for multiple possible measurements - return self.measurements[0] - - def finalize(self): - """Called when outermost measurement is finished""" - - def create_dataset(self): - pass - - def new_dataset(self): - pass + setpoint_parameters = [] + setpoints_action_indices = [] + for k in range(len(action_indices) + 1): + if action_indices[:k] in self.setpoint_list: + setpoint_parameter = self.setpoint_list[action_indices[:k]]['parameter'] + setpoint_parameters.append(setpoint_parameter) + setpoints_action_indices.append(action_indices[:k]) + + measurement_info = { + 'parameter': parameter, + 'setpoints_action_indices': setpoints_action_indices, + 'setpoint_parameters': setpoint_parameters, + 'shape': self.measurement_loop.loop_shape, + 'unstored_results': [] + } - def add_metadata(self): - pass + return measurement_info def add_measurement_result( self, @@ -219,246 +187,43 @@ def add_measurement_result( ) # Get parameter data array, creating a new one if necessary - if action_indices not in self.data_arrays: - # Create array based on first result type and shape - self._create_data_array( - action_indices, - result, + if action_indices not in self.measurement_list: + measurement_info = self.create_meaasurement_info( + action_indices=action_indices, parameter=parameter, name=name, label=label, - unit=unit, + unit=unit ) + self.measurement_list[action_indices] = measurement_info - # Select existing array - data_array = self.data_arrays[action_indices] + measurement_info = self.measurement_list[action_indices] - # Ensure an existing data array has the correct name - # parameter can also be a string, in which case we don't use parameter.name - if name is None: + if name is None and parameter is not None: name = parameter.name - - # TODO is this the right place for this check? - if not data_array.name == name: - raise SyntaxError( - f"Existing DataArray '{data_array.name}' differs from result {name}" - ) - - data_to_store = {data_array.array_id: result} - - # If result is an array, update set_array elements - if isinstance(result, list): # Convert result list to array - result = np.ndarray(result) - if isinstance(result, np.ndarray): - ndim = len(self.loop_indices) - if len(data_array.set_arrays) != ndim + result.ndim: - raise RuntimeError( - f"Wrong number of set arrays for {data_array.name}. " - f"Expected {ndim + result.ndim} instead of " - f"{len(data_array.set_arrays)}." - ) - - for k, set_array in enumerate(data_array.set_arrays[ndim:]): - # Successive set arrays must increase dimensionality by unity - arr = np.arange(result.shape[k]) - if parameter is not None and hasattr(parameter, 'setpoints') \ - and parameter.setpoints is not None: - arr_idx = parameter.names.index(name) - arr = parameter.setpoints[arr_idx][k] - - # Add singleton dimensions - arr = np.broadcast_to(arr, result.shape[: k + 1]) - data_to_store[set_array.array_id] = arr - - # Use dummy index if there are no loop indices. - # This happens if the measurement is performed outside a Sweep - loop_indices = self.loop_indices - if not loop_indices and not isinstance(result, (list, np.ndarray)): - loop_indices = (0,) - - return data_to_store - - # Data array functions - # TODO Needs to be reformed - def _create_data_array( - self, - action_indices: Tuple[int], - result, - parameter: Parameter = None, - is_setpoint: bool = False, - name: str = None, - label: str = None, - unit: str = None, - ): - """Create a data array from a parameter and result. - - The data array shape is extracted from the result shape, and the current - loop dimensions. - - The data array is added to the current data set. - - Args: - parameter: Parameter for which to create a DataArray. Can also be a - string, in which case it is the data_array name - result: Result returned by the Parameter - action_indices: Action indices for which to store parameter - is_setpoint: Whether the Parameter is used for sweeping or measuring - label: Data array label. If not provided, the parameter label is - used. If the parameter is a name string, the label is extracted - from the name. - unit: Data array unit. If not provided, the parameter unit is used. - - Returns: - Newly created data array - - """ - if parameter is None and name is None: + if name != measurement_info['parameter'].name: raise SyntaxError( - "When creating a data array, must provide either a parameter or a name" + f'Provided name {name} must match that of previous measurement ' + f"{measurement_info['parameter'].name}" ) - if len(running_measurement().data_arrays) >= self.max_arrays: - raise RuntimeError( - f"Number of arrays in dataset exceeds " - f"Measurement.max_arrays={self.max_arrays}. Perhaps you forgot" - f"to encapsulate a loop with a Sweep()?" - ) - - array_kwargs = { - "is_setpoint": is_setpoint, - "action_indices": action_indices, - "shape": self.loop_shape, - } - - if is_setpoint or isinstance(result, (np.ndarray, list)): - array_kwargs["shape"] += np.shape(result) - - # Use dummy index (1, ) if measurement is performed outside a Sweep - if not array_kwargs["shape"]: - array_kwargs["shape"] = (1,) - - if isinstance(parameter, Parameter): - array_kwargs["parameter"] = parameter - # Add a custom name - if name is not None: - array_kwargs["full_name"] = name - if label is not None: - array_kwargs["label"] = label - if unit is not None: - array_kwargs["unit"] = unit - else: - array_kwargs["name"] = name - if label is None: - label = name[0].capitalize() + name[1:].replace("_", " ") - array_kwargs["label"] = label - array_kwargs["unit"] = unit or "" - - # Add setpoint arrays - if not is_setpoint: - array_kwargs["set_arrays"] = self._add_set_arrays( - action_indices, result, parameter=parameter, name=(name or parameter.name) + # Store result + setpoints = [ + self.setpoint_list[action_indices]['latest_value'] + for action_indices in measurement_info['setpoints_action_indices'] + ] + result_with_setpoints = tuple(zip(parameters, (*setpoints, result))) + if self.initialized: + parameters = ( + *measurement_info['setpoint_parameters'], + measurement_info['dataset_parameter'] ) - - data_array = DataArray(**array_kwargs) - - data_array.array_id = data_array.full_name - data_array.array_id += "_" + "_".join(str(k) for k in action_indices) - - data_array.init_data() - - self.dataset.add_array(data_array) - with self.timings.record(['dataset', 'save_metadata']): - self.dataset.save_metadata() - - # Add array to set_arrays or to data_arrays of this Measurement - if is_setpoint: - self.set_arrays[action_indices] = data_array + + self.dataset.add_result(result_with_setpoints) else: - self.data_arrays[action_indices] = data_array - - return data_array - - def _add_set_arrays( - self, action_indices: Tuple[int], result, name: str, parameter: Union[Parameter, None] = None - ): - """Create set arrays for a given action index""" - set_arrays = [] - for k in range(1, len(action_indices)): - sweep_indices = action_indices[:k] - - if sweep_indices in self.set_arrays: - set_arrays.append(self.set_arrays[sweep_indices]) - # TODO handle grouped arrays (e.g. ParameterNode, nested Measurement) - # Create new set array(s) if parameter result is an array or list - if isinstance(result, (np.ndarray, list)): - if isinstance(result, list): - result = np.ndarray(result) - - for k, shape in enumerate(result.shape): - arr = np.arange(shape) - label = None - unit = None - if parameter is not None and hasattr(parameter, 'setpoints') \ - and parameter.setpoints is not None: - arr_idx = parameter.names.index(name) - arr = parameter.setpoints[arr_idx][k] - label = parameter.setpoint_labels[arr_idx][k] - unit = parameter.setpoint_units[arr_idx][k] - - # Add singleton dimensions - arr = np.broadcast_to(arr, result.shape[: k + 1]) - - set_array = self._create_data_array( - action_indices=action_indices + (0,) * k, - result=arr, - name=f"{name}_set{k}", - label=label, - unit=unit, - is_setpoint=True, - ) - set_arrays.append(set_array) - - # Add a dummy array in case the measurement was performed outside of - # a Sweep. This is not needed if the result is an array - if not set_arrays and not self.loop_indices: - set_arrays = [ - self._create_data_array( - action_indices=running_measurement().action_indices, - result=result, - name="None", - is_setpoint=True, - ) - ] - set_arrays[0][0] = 1 - - return tuple(set_arrays) - - def get_arrays(self, action_indices: Sequence[int] = None) -> List[DataArray]: - """Get all arrays belonging to the current action indices - - If the action indices corresponds to a group of arrays (e.g. a nested - measurement or ParameterNode), all the arrays in the group are returned - - Args: - action_indices: Action indices of arrays. - If not provided, the current action_indices are chosen - - Returns: - List of data arrays matching the action indices - """ - if action_indices is None: - action_indices = self.action_indices - - if not isinstance(action_indices, Sequence): - raise SyntaxError("parent_action_indices must be a tuple") - - num_indices = len(action_indices) - return [ - arr - for action_indices, arr in self.data_arrays.items() - if action_indices[:num_indices] == action_indices - ] - + measurement_info['unstored_results'].append(result_with_setpoints) + # Also store in measurement_info + measurement_info['latest_value'] = result class MeasurementLoop: """Class to perform measurements @@ -603,7 +368,7 @@ def __enter__(self): MeasurementLoop.measurement_thread = threading.current_thread() # Initialize dataset handler - self.data_handler = DataHandler() + self.data_handler = DatasetHandler(measurement_loop=self) # TODO incorporate metadata # self._initialize_metadata(self.dataset) @@ -1023,8 +788,6 @@ def measure( 'T_pre', unit='s', timestamp=False) self.skip() # Increment last action index by 1 - - # TODO Incorporate kwargs name, label, and unit, into each of these if isinstance(measurable, Parameter): result = self._measure_parameter( From 575ede2e2e3b0fcc9738a42c690d429f5a909ba0 Mon Sep 17 00:00:00 2001 From: Serwan Date: Sat, 2 Apr 2022 20:56:23 +0200 Subject: [PATCH 010/122] creating tests for measurement loop --- qcodes/tests/dataset/measurement_loop/__init__.py | 8 ++++++++ .../test_measurement_loop_basics.py | 13 +++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 qcodes/tests/dataset/measurement_loop/__init__.py create mode 100644 qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py diff --git a/qcodes/tests/dataset/measurement_loop/__init__.py b/qcodes/tests/dataset/measurement_loop/__init__.py new file mode 100644 index 00000000000..b4c82335d6a --- /dev/null +++ b/qcodes/tests/dataset/measurement_loop/__init__.py @@ -0,0 +1,8 @@ + +import pytest + + +from qcodes.dataset.measurement import Measurement, Sweep + + + diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py new file mode 100644 index 00000000000..e552836241b --- /dev/null +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -0,0 +1,13 @@ +import math +import pytest + +def test_sqrt(): + num = 25 + assert math.sqrt(num) == 5 + +def testsquare(): + num = 7 + assert 7*7 == 40 + +def testequality(): + assert 10 == 11 From 1f0171bf6c41078e45b21322eb3a27df7aebd917 Mon Sep 17 00:00:00 2001 From: Serwan Date: Sun, 3 Apr 2022 12:18:33 +0200 Subject: [PATCH 011/122] working 1d and 2d datasets --- qcodes/dataset/measurement_loop.py | 202 +++++++++++------- .../dataset/measurement_loop/__init__.py | 8 - .../test_measurement_loop_basics.py | 76 ++++++- 3 files changed, 192 insertions(+), 94 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index e89aff90068..42e5962d701 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1,3 +1,4 @@ +import contextlib from enum import unique import numpy as np from collections import Counter @@ -10,7 +11,6 @@ from qcodes.dataset.measurements import Measurement from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement -from qcodes.tests.dataset import measurement from qcodes.utils.dataset.doNd import AbstractSweep from qcodes.station import Station from qcodes.instrument.base import InstrumentBase @@ -22,7 +22,7 @@ get_last_input_cells, PerformanceTimer ) -from qcodes import config as qcodes_config, measure +from qcodes import config as qcodes_config RAW_VALUE_TYPES = (float, int, bool, np.ndarray, np.integer, np.floating, np.bool_, type(None)) @@ -33,9 +33,10 @@ def __init__(self, measurement_loop): self.measurement_loop = measurement_loop self.initialized = False - self.dataset = None + self.datasaver = None self.runner = None self.measurement = None + self.dataset = None # Key: action_index # Values: @@ -53,6 +54,7 @@ def __init__(self, measurement_loop): # - unstored_results - list where each element contains (*setpoints, measurement_value) # - latest_value + # TODO should be called at appropriate time def initialize(self): # Once initialized, no new parameters can be added assert not self.initialized, "Cannot initialize twice" @@ -63,7 +65,14 @@ def initialize(self): self._create_unique_dataset_parameters(self.setpoint_list) for setpoint_info in self.setpoint_list.values(): self.measurement.register_parameter(setpoint_info['dataset_parameter']) - + + # Determine setpoint_parameters for each measurement_parameter + for measurement_info in self.measurement_list.values(): + measurement_info['setpoint_parameters'] = tuple( + self.setpoint_list[action_indices]['dataset_parameter'] + for action_indices in measurement_info['setpoints_action_indices'] + ) + # Register all measurement parameters self._create_unique_dataset_parameters(self.measurement_list) for measurement_info in self.measurement_list.values(): @@ -82,29 +91,34 @@ def initialize(self): self.runner = self.measurement.run() # Create measurement Dataset - self.dataset = self.runner.__enter__() + self.datasaver = self.runner.__enter__() + self.dataset = self.datasaver.dataset # Add results that were taken before initializing dataset for measurement_info in self.measurement_list.values(): for unstored_result in measurement_info['unstored_results']: parameters = *measurement_info['setpoint_parameters'], measurement_info['dataset_parameter'] result = tuple(zip(parameters, unstored_result)) - self.dataset.add_result(result) + self.datasaver.add_result(*result) self.initialized = True + def finalize(self): + if not self.initialized: + self.initialize() + def _create_unique_dataset_parameters(self, parameter_list): """Populates 'dataset_parameter' of parameter_list Ensure parameters have unique names """ - parameter_names = [param_info['parameter'].name for param_info in parameter_list] - duplicate_names = [name for name, count in Counter(parameter_names) if count > 1] - unique_names = [name for name, count in Counter(parameter_names) if count == 1] + parameter_names = [param_info['parameter'].name for param_info in parameter_list.values()] + duplicate_names = [name for name, count in Counter(parameter_names).items() if count > 1] + unique_names = [name for name, count in Counter(parameter_names).items() if count == 1] for name in unique_names: parameter_info = next( - param_info for param_info in parameter_list + param_info for param_info in parameter_list.values() if param_info['parameter'].name == name ) parameter_info['dataset_parameter'] = parameter_info['parameter'] @@ -112,7 +126,7 @@ def _create_unique_dataset_parameters(self, parameter_list): for name in duplicate_names: # Need to rename parameters with duplicate names duplicate_parameter_info_list = [ - param_info for param_info in parameter_list + param_info for param_info in parameter_list.values() if param_info['parameter'].name == name ] for k, parameter_info in duplicate_parameter_info_list: @@ -142,24 +156,20 @@ def create_measurement_info( 'label': label, 'unit': unit } - overwrite_attrs = {key: val for key, val in overwrite_attrs if val is not None} + overwrite_attrs = {key: val for key, val in overwrite_attrs.items() if val is not None} parameter = DelegateParameter( source=parameter, **overwrite_attrs ) - setpoint_parameters = [] setpoints_action_indices = [] for k in range(len(action_indices) + 1): if action_indices[:k] in self.setpoint_list: - setpoint_parameter = self.setpoint_list[action_indices[:k]]['parameter'] - setpoint_parameters.append(setpoint_parameter) setpoints_action_indices.append(action_indices[:k]) measurement_info = { 'parameter': parameter, 'setpoints_action_indices': setpoints_action_indices, - 'setpoint_parameters': setpoint_parameters, 'shape': self.measurement_loop.loop_shape, 'unstored_results': [] } @@ -188,7 +198,7 @@ def add_measurement_result( # Get parameter data array, creating a new one if necessary if action_indices not in self.measurement_list: - measurement_info = self.create_meaasurement_info( + measurement_info = self.create_measurement_info( action_indices=action_indices, parameter=parameter, name=name, @@ -212,16 +222,17 @@ def add_measurement_result( self.setpoint_list[action_indices]['latest_value'] for action_indices in measurement_info['setpoints_action_indices'] ] - result_with_setpoints = tuple(zip(parameters, (*setpoints, result))) + if self.initialized: parameters = ( *measurement_info['setpoint_parameters'], measurement_info['dataset_parameter'] ) - self.dataset.add_result(result_with_setpoints) + result_with_setpoints = tuple(zip(parameters, (*setpoints, result))) + self.datasaver.add_result(result_with_setpoints) else: - measurement_info['unstored_results'].append(result_with_setpoints) + measurement_info['unstored_results'].append((*setpoints, result)) # Also store in measurement_info measurement_info['latest_value'] = result @@ -309,6 +320,10 @@ def __init__(self, name: str, force_cell_thread: bool = True, notify=False): self.timings = PerformanceTimer() + @property + def dataset(self): + return self.data_handler.dataset + def log(self, message: str, level="info"): """Send a log message @@ -385,8 +400,8 @@ def __enter__(self): self.data_arrays = {} self.set_arrays = {} - self.log(f'Measurement started {self.dataset.location}') - print(f'Measurement started {self.dataset.location}') + # self.log(f'Measurement started {self.dataset.location}') + # print(f'Measurement started {self.dataset.location}') else: if threading.current_thread() is not MeasurementLoop.measurement_thread: @@ -482,8 +497,10 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb): self.log("Could not notify", level="error") t_stop = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - self.data_handler.add_metadata({"t_stop": t_stop}) - self.data_handler.add_metadata({"timings": self.timings}) + + # TODO include metadata + # self.data_handler.add_metadata({"t_stop": t_stop}) + # self.data_handler.add_metadata({"timings": self.timings}) self.data_handler.finalize() self.log(f'Measurement finished') @@ -494,39 +511,39 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb): self.is_context_manager = False # TODO Needs to be implemented - def _initialize_metadata(self, dataset): - """Initialize dataset metadata""" - if dataset is None: - dataset = self.dataset - - config = qcodes_config - dataset.add_metadata({"config": config}) - - dataset.add_metadata({"measurement_type": "Measurement"}) - - # Add instrument information - if Station.default is not None: - dataset.add_metadata({"station": Station.default.snapshot()}) - - if using_ipython(): - measurement_cell = get_last_input_cells(1)[0] - - measurement_code = measurement_cell - # If the code is run from a measurement thread, there is some - # initial code that should be stripped - init_string = "get_ipython().run_cell_magic('new_job', '', " - if measurement_code.startswith(init_string): - measurement_code = measurement_code[len(init_string) + 1 : -4] - - self._t_start = datetime.now() - dataset.add_metadata( - { - "measurement_cell": measurement_cell, - "measurement_code": measurement_code, - "last_input_cells": get_last_input_cells(20), - "t_start": self._t_start.strftime('%Y-%m-%d %H:%M:%S') - } - ) + # def _initialize_metadata(self, dataset): + # """Initialize dataset metadata""" + # if dataset is None: + # dataset = self.dataset + + # config = qcodes_config + # dataset.add_metadata({"config": config}) + + # dataset.add_metadata({"measurement_type": "Measurement"}) + + # # Add instrument information + # if Station.default is not None: + # dataset.add_metadata({"station": Station.default.snapshot()}) + + # if using_ipython(): + # measurement_cell = get_last_input_cells(1)[0] + + # measurement_code = measurement_cell + # # If the code is run from a measurement thread, there is some + # # initial code that should be stripped + # init_string = "get_ipython().run_cell_magic('new_job', '', " + # if measurement_code.startswith(init_string): + # measurement_code = measurement_code[len(init_string) + 1 : -4] + + # self._t_start = datetime.now() + # dataset.add_metadata( + # { + # "measurement_cell": measurement_cell, + # "measurement_code": measurement_code, + # "last_input_cells": get_last_input_cells(20), + # "t_start": self._t_start.strftime('%Y-%m-%d %H:%M:%S') + # } + # ) def _verify_action(self, action, name, add_if_new=True): """Verify an action corresponds to the current action indices. @@ -1105,6 +1122,33 @@ def running_measurement() -> MeasurementLoop: return MeasurementLoop.running_measurement +class _IterateDondSweep: + def __init__(self, sweep: AbstractSweep): + self.sweep = sweep + self.iterator = None + self.parameter = sweep._param + + def __len__(self): + return self.sweep.num_points + + def __iter__(self): + self.iterator = iter(self.sweep.get_setpoints()) + return self + + def __next__(self): + value = next(self.iterator) + self.sweep._param(value) + + for action in self.sweep.post_actions: + action() + + if self.sweep.delay: + sleep(self.sweep.delay) + + return value + + + class Sweep: """Sweep over an iterable inside a Measurement @@ -1133,7 +1177,9 @@ def __init__(self, sequence, name=None, label=None, unit=None, reverse=False, re if running_measurement() is None: raise RuntimeError("Cannot create a sweep outside a Measurement") - if not isinstance(sequence, Iterable): + if isinstance(sequence, AbstractSweep): + sequence = _IterateDondSweep(sequence) + elif not isinstance(sequence, Iterable): raise SyntaxError("Sweep sequence must be iterable") # Properties for the data array @@ -1164,6 +1210,7 @@ def __iter__(self): running_measurement().mask(self.sequence.parameter, self.sequence.parameter.get()) else: raise NotImplementedError("Unable to restore non-parameter values.") + if self.reverse: self.loop_index = len(self.sequence) - 1 self.iterator = iter(self.sequence[::-1]) @@ -1223,29 +1270,30 @@ def __next__(self): def initialize(self): msmt = running_measurement() - assert msmt.action_indices not in msmt.setpoint_list, f"Setpoint {self.name} already initialized" - - # Determine sweep parameter - if isinstance(self.sequence, AbstractSweep) and hasattr(self.sequence, '_param'): - # sweep is a doNd sweep that already has a parameter - set_parameter = self.sequence._param + if msmt.action_indices in msmt.setpoint_list: + return msmt.setpoint_list[msmt.action_indices] else: - # Need to create a parameter - set_parameter = Parameter( - name=self.name, - label=self.label, - unit=self.unit - ) + # Determine sweep parameter + if isinstance(self.sequence, _IterateDondSweep): + # sweep is a doNd sweep that already has a parameter + set_parameter = self.sequence.parameter + else: + # Need to create a parameter + set_parameter = Parameter( + name=self.name, + label=self.label, + unit=self.unit + ) - setpoint_info = { - 'parameter': set_parameter, - 'latest_value': None - } + setpoint_info = { + 'parameter': set_parameter, + 'latest_value': None + } - # Add to setpoint list - msmt.setpoint_list[msmt.action_indices] = setpoint_info + # Add to setpoint list + msmt.setpoint_list[msmt.action_indices] = setpoint_info - return setpoint_info + return setpoint_info def exit_sweep(self): msmt = running_measurement() diff --git a/qcodes/tests/dataset/measurement_loop/__init__.py b/qcodes/tests/dataset/measurement_loop/__init__.py index b4c82335d6a..e69de29bb2d 100644 --- a/qcodes/tests/dataset/measurement_loop/__init__.py +++ b/qcodes/tests/dataset/measurement_loop/__init__.py @@ -1,8 +0,0 @@ - -import pytest - - -from qcodes.dataset.measurement import Measurement, Sweep - - - diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index e552836241b..662da798dd7 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -1,13 +1,71 @@ -import math +import shutil import pytest +import contextlib +import tempfile +from pathlib import Path -def test_sqrt(): - num = 25 - assert math.sqrt(num) == 5 +from qcodes import Parameter, ManualParameter +from qcodes.utils.dataset.doNd import LinSweep +from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep +from qcodes.dataset import ( + initialise_or_create_database_at, + load_by_run_spec, + load_or_create_experiment, +) -def testsquare(): - num = 7 - assert 7*7 == 40 +# def get_data_array(dataset, label): -def testequality(): - assert 10 == 11 + + +@pytest.fixture +def create_dummy_database(): + + @contextlib.contextmanager + def func_context_manager(): + with tempfile.TemporaryDirectory() as temporary_folder: + temporary_folder = tempfile.TemporaryDirectory() + print(f'Created temporary folder for database: {temporary_folder}') + + assert Path(temporary_folder.name).exists() + db_path = Path(temporary_folder.name) / 'test_database.db' + initialise_or_create_database_at(str(db_path)) + + yield load_or_create_experiment("test_experiment") + return func_context_manager + + +def test_create_measurement(create_dummy_database): + with create_dummy_database(): + measurement_loop = MeasurementLoop('test') + + +def test_basic_1D_measurement(create_dummy_database): + with create_dummy_database(): + # Initialize parameters + p1_get = ManualParameter('p1_get') + p1_set = ManualParameter('p1_set') + + with MeasurementLoop('test') as msmt: + for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + assert p1_set() == val + p1_get(val+1) + msmt.measure(p1_get) + + + + +def test_basic_2D_measurement(create_dummy_database): + with create_dummy_database(): + # Initialize parameters + p1_get = ManualParameter('p1_get') + p1_set = ManualParameter('p1_set') + p2_set = ManualParameter('p2_set') + + with MeasurementLoop('test') as msmt: + for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + assert p1_set() == val + for val2 in Sweep(LinSweep(p2_set, 0, 1, 11)): + assert p2_set() == val2 + p1_get(val+1) + msmt.measure(p1_get) + print('finished') \ No newline at end of file From 575568674cfefecf806fab046ad70b5ec56ce057 Mon Sep 17 00:00:00 2001 From: Serwan Date: Sun, 3 Apr 2022 12:57:11 +0200 Subject: [PATCH 012/122] basic 1d and 2d tests are working! --- qcodes/dataset/measurement_loop.py | 14 +- .../test_measurement_loop_basics.py | 135 +++++++++++++++++- 2 files changed, 142 insertions(+), 7 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 42e5962d701..09a8c99c7f1 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -29,8 +29,9 @@ class DatasetHandler: """Handler for a single DataSet (with Measurement and Runner)""" - def __init__(self, measurement_loop): + def __init__(self, measurement_loop, name='results'): self.measurement_loop = measurement_loop + self.name = name self.initialized = False self.datasaver = None @@ -59,7 +60,7 @@ def initialize(self): # Once initialized, no new parameters can be added assert not self.initialized, "Cannot initialize twice" - self.measurement = Measurement() + self.measurement = Measurement(name=self.name) # Register all setpoints parameters self._create_unique_dataset_parameters(self.setpoint_list) @@ -107,6 +108,8 @@ def finalize(self): if not self.initialized: self.initialize() + self.datasaver.flush_data_to_database() + def _create_unique_dataset_parameters(self, parameter_list): """Populates 'dataset_parameter' of parameter_list @@ -129,7 +132,7 @@ def _create_unique_dataset_parameters(self, parameter_list): param_info for param_info in parameter_list.values() if param_info['parameter'].name == name ] - for k, parameter_info in duplicate_parameter_info_list: + for k, parameter_info in enumerate(duplicate_parameter_info_list): # Create delegate parameter delegate_parameter = DelegateParameter( name=f"{parameter_info['parameter'].name}_{k}", @@ -383,7 +386,10 @@ def __enter__(self): MeasurementLoop.measurement_thread = threading.current_thread() # Initialize dataset handler - self.data_handler = DatasetHandler(measurement_loop=self) + self.data_handler = DatasetHandler( + measurement_loop=self, + name=self.name + ) # TODO incorporate metadata # self._initialize_metadata(self.dataset) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 662da798dd7..98f1617065e 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -1,3 +1,4 @@ +import numpy as np import shutil import pytest import contextlib @@ -36,7 +37,7 @@ def func_context_manager(): def test_create_measurement(create_dummy_database): with create_dummy_database(): - measurement_loop = MeasurementLoop('test') + MeasurementLoop('test') def test_basic_1D_measurement(create_dummy_database): @@ -51,9 +52,23 @@ def test_basic_1D_measurement(create_dummy_database): p1_get(val+1) msmt.measure(p1_get) - + data = msmt.dataset + assert data.name == 'test' + assert data.parameters == 'p1_set,p1_get' + + arrays = data.get_parameter_data() + data_arrays = arrays['p1_get'] + assert np.allclose( + data_arrays['p1_get'], + np.linspace(1, 2, 11) + ) + assert np.allclose( + data_arrays['p1_set'], + np.linspace(0, 1, 11) + ) + def test_basic_2D_measurement(create_dummy_database): with create_dummy_database(): # Initialize parameters @@ -68,4 +83,118 @@ def test_basic_2D_measurement(create_dummy_database): assert p2_set() == val2 p1_get(val+1) msmt.measure(p1_get) - print('finished') \ No newline at end of file + + data = msmt.dataset + assert data.name == 'test' + assert data.parameters == 'p1_set,p2_set,p1_get' + + arrays = data.get_parameter_data() + data_array = arrays['p1_get']['p1_get'] + + assert np.allclose( + data_array, + np.tile(np.linspace(1, 2, 11), (11,1)).transpose() + ) + + assert np.allclose( + arrays['p1_get']['p1_set'], + np.tile(np.linspace(0, 1, 11), (11,1)).transpose() + ) + + assert np.allclose( + arrays['p1_get']['p2_set'], + np.tile(np.linspace(0, 1, 11), (11,1)) + ) + + +def test_1D_measurement_duplicate_get(create_dummy_database): + with create_dummy_database(): + # Initialize parameters + p1_get = ManualParameter('p1_get') + p1_set = ManualParameter('p1_set') + + with MeasurementLoop('test') as msmt: + for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + assert p1_set() == val + p1_get(val+1) + msmt.measure(p1_get) + p1_get(val+0.5) + msmt.measure(p1_get) + + data = msmt.dataset + assert data.name == 'test' + assert data.parameters == 'p1_set,p1_get_0,p1_get_1' + + arrays = data.get_parameter_data() + + offsets = {'p1_get_0': 1, 'p1_get_1': 0.5} + for key in ['p1_get_0', 'p1_get_1']: + data_arrays = arrays[key] + + assert np.allclose( + data_arrays[key], + np.linspace(0, 1, 11) + offsets[key] + ) + assert np.allclose( + data_arrays['p1_set'], + np.linspace(0, 1, 11) + ) + + +def test_1D_measurement_duplicate_getset(create_dummy_database): + with create_dummy_database(): + # Initialize parameters + p1_get = ManualParameter('p1_get') + p1_set = ManualParameter('p1_set') + + with MeasurementLoop('test') as msmt: + for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + assert p1_set() == val + p1_get(val+1) + msmt.measure(p1_get) + for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + assert p1_set() == val + p1_get(val+0.5) + msmt.measure(p1_get) + + data = msmt.dataset + assert data.name == 'test' + assert data.parameters == 'p1_set_0,p1_set_1,p1_get_0,p1_get_1' + + arrays = data.get_parameter_data() + + offsets = {'p1_get_0': 1, 'p1_get_1': 0.5} + for k in [0, 1]: + get_key = f'p1_get_{k}' + set_key = f'p1_set_{k}' + data_arrays = arrays[get_key] + + assert np.allclose( + data_arrays[get_key], + np.linspace(0, 1, 11) + offsets[get_key] + ) + assert np.allclose( + data_arrays[set_key], + np.linspace(0, 1, 11) + ) + + +def test_2D_measurement_initialization(create_dummy_database): + with create_dummy_database(): + # Initialize parameters + p1_get = ManualParameter('p1_get') + p1_set = ManualParameter('p1_set') + p2_set = ManualParameter('p2_set') + + with MeasurementLoop('test') as msmt: + for k, val in enumerate(Sweep(LinSweep(p1_set, 0, 1, 11))): + assert p1_set() == val + for val2 in Sweep(LinSweep(p2_set, 0, 1, 11)): + assert p2_set() == val2 + p1_get(val+1) + msmt.measure(p1_get) + + if not k: + assert not msmt.data_handler.initialized + else: + assert msmt.data_handler.initialized \ No newline at end of file From a2852de3f15674491f12c6927a2e433f5717b76b Mon Sep 17 00:00:00 2001 From: Serwan Date: Sun, 3 Apr 2022 13:16:25 +0200 Subject: [PATCH 013/122] initializes dataset on second iteration of first sweep --- qcodes/dataset/measurement_loop.py | 25 ++++++++++++++++--- .../test_measurement_loop_basics.py | 10 ++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 09a8c99c7f1..1ae60e10342 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -233,7 +233,7 @@ def add_measurement_result( ) result_with_setpoints = tuple(zip(parameters, (*setpoints, result))) - self.datasaver.add_result(result_with_setpoints) + self.datasaver.add_result(*result_with_setpoints) else: measurement_info['unstored_results'].append((*setpoints, result)) # Also store in measurement_info @@ -1180,7 +1180,8 @@ class Sweep: ``` """ def __init__(self, sequence, name=None, label=None, unit=None, reverse=False, restore=False): - if running_measurement() is None: + msmt = running_measurement() + if msmt is None: raise RuntimeError("Cannot create a sweep outside a Measurement") if isinstance(sequence, AbstractSweep): @@ -1200,9 +1201,13 @@ def __init__(self, sequence, name=None, label=None, unit=None, reverse=False, re self.reverse = reverse self.restore = restore + # Check if this is the first sweep + # Useful to know when to initialize dataset + msmt = running_measurement() + self.is_first_sweep = not any(isinstance(action, Sweep) for action in msmt.actions.values()) + # Create setpoint_list self.initialize() - msmt = running_measurement() self.setpoint_info = msmt.setpoint_list[msmt.action_indices] def __iter__(self): @@ -1241,6 +1246,16 @@ def __next__(self): elif msmt.is_stopped: raise SystemExit + # Initialize data handler if the first sweep reaches its second iteration + # would be nicer if the sweep doesn't talk to the data handler + if self.is_first_sweep: + if ( + (self.reverse and self.loop_index == len(self.sequence) - 2) + or (not self.reverse and self.loop_index == 1) + ): + if not msmt.data_handler.initialized: + msmt.data_handler.initialize() + # Wait as long as the measurement is paused while msmt.is_paused: sleep(0.1) @@ -1299,6 +1314,10 @@ def initialize(self): # Add to setpoint list msmt.setpoint_list[msmt.action_indices] = setpoint_info + # Add to measurement actions + assert msmt.action_indices not in msmt.actions + msmt.actions[msmt.action_indices] = self + return setpoint_info def exit_sweep(self): diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 98f1617065e..49d666f6976 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -187,9 +187,15 @@ def test_2D_measurement_initialization(create_dummy_database): p2_set = ManualParameter('p2_set') with MeasurementLoop('test') as msmt: - for k, val in enumerate(Sweep(LinSweep(p1_set, 0, 1, 11))): + outer_sweep = Sweep(LinSweep(p1_set, 0, 1, 11)) + for k, val in enumerate(outer_sweep): assert p1_set() == val - for val2 in Sweep(LinSweep(p2_set, 0, 1, 11)): + assert outer_sweep.is_first_sweep + + inner_sweep = Sweep(LinSweep(p2_set, 0, 1, 11)) + assert not inner_sweep.is_first_sweep + + for val2 in inner_sweep: assert p2_set() == val2 p1_get(val+1) msmt.measure(p1_get) From 124e55052a05cc1047782c3076178c2fb7dd8582 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 18:55:09 +0000 Subject: [PATCH 014/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 69 ++-- .../test_measurement_loop_basics.py | 329 ++++++++---------- qcodes/utils/helpers.py | 6 +- 3 files changed, 189 insertions(+), 215 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 1ae60e10342..8efdf1d7d4a 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1,28 +1,29 @@ import contextlib -from enum import unique -import numpy as np -from collections import Counter -from typing import List, Tuple, Union, Sequence, Dict, Any, Callable, Iterable +import logging import threading -from time import sleep, perf_counter import traceback -import logging +from collections import Counter from datetime import datetime +from enum import unique +from time import perf_counter, sleep +from typing import Any, Callable, Dict, Iterable, List, Sequence, Tuple, Union -from qcodes.dataset.measurements import Measurement +import numpy as np + +from qcodes import config as qcodes_config from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement -from qcodes.utils.dataset.doNd import AbstractSweep -from qcodes.station import Station +from qcodes.dataset.measurements import Measurement from qcodes.instrument.base import InstrumentBase +from qcodes.instrument.parameter import DelegateParameter, MultiParameter, Parameter from qcodes.instrument.sweep_values import SweepValues -from qcodes.instrument.parameter import DelegateParameter, Parameter, MultiParameter +from qcodes.station import Station +from qcodes.utils.dataset.doNd import AbstractSweep from qcodes.utils.helpers import ( - using_ipython, + PerformanceTimer, directly_executed_from_cell, get_last_input_cells, - PerformanceTimer + using_ipython, ) -from qcodes import config as qcodes_config RAW_VALUE_TYPES = (float, int, bool, np.ndarray, np.integer, np.floating, np.bool_, type(None)) @@ -67,7 +68,7 @@ def initialize(self): for setpoint_info in self.setpoint_list.values(): self.measurement.register_parameter(setpoint_info['dataset_parameter']) - # Determine setpoint_parameters for each measurement_parameter + # Determine setpoint_parameters for each measurement_parameter for measurement_info in self.measurement_list.values(): measurement_info['setpoint_parameters'] = tuple( self.setpoint_list[action_indices]['dataset_parameter'] @@ -83,8 +84,7 @@ def initialize(self): ) self.measurement.set_shapes( detect_shape_of_measurement( - (measurement_info['dataset_parameter'],), - measurement_info['shape'] + (measurement_info["dataset_parameter"],), measurement_info["shape"] ) ) @@ -112,7 +112,7 @@ def finalize(self): def _create_unique_dataset_parameters(self, parameter_list): """Populates 'dataset_parameter' of parameter_list - + Ensure parameters have unique names """ parameter_names = [param_info['parameter'].name for param_info in parameter_list.values()] @@ -141,12 +141,7 @@ def _create_unique_dataset_parameters(self, parameter_list): parameter_info['dataset_parameter'] = delegate_parameter def create_measurement_info( - self, - action_indices, - parameter, - name=None, - label=None, - unit=None + self, action_indices, parameter, name=None, label=None, unit=None ): assert not self.initialized, "Cannot measure parameter for the first time after initializing dataset" @@ -180,9 +175,9 @@ def create_measurement_info( return measurement_info def add_measurement_result( - self, - action_indices, - result, + self, + action_indices, + result, parameter=None, name: str = None, label: str = None, @@ -228,16 +223,17 @@ def add_measurement_result( if self.initialized: parameters = ( - *measurement_info['setpoint_parameters'], - measurement_info['dataset_parameter'] + *measurement_info["setpoint_parameters"], + measurement_info["dataset_parameter"], ) - + result_with_setpoints = tuple(zip(parameters, (*setpoints, result))) self.datasaver.add_result(*result_with_setpoints) else: measurement_info['unstored_results'].append((*setpoints, result)) # Also store in measurement_info - measurement_info['latest_value'] = result + measurement_info["latest_value"] = result + class MeasurementLoop: """Class to perform measurements @@ -1144,13 +1140,13 @@ def __iter__(self): def __next__(self): value = next(self.iterator) self.sweep._param(value) - + for action in self.sweep.post_actions: action() if self.sweep.delay: sleep(self.sweep.delay) - + return value @@ -1180,7 +1176,7 @@ class Sweep: ``` """ def __init__(self, sequence, name=None, label=None, unit=None, reverse=False, restore=False): - msmt = running_measurement() + msmt = running_measurement() if msmt is None: raise RuntimeError("Cannot create a sweep outside a Measurement") @@ -1249,9 +1245,8 @@ def __next__(self): # Initialize data handler if the first sweep reaches its second iteration # would be nicer if the sweep doesn't talk to the data handler if self.is_first_sweep: - if ( - (self.reverse and self.loop_index == len(self.sequence) - 2) - or (not self.reverse and self.loop_index == 1) + if (self.reverse and self.loop_index == len(self.sequence) - 2) or ( + not self.reverse and self.loop_index == 1 ): if not msmt.data_handler.initialized: msmt.data_handler.initialize() @@ -1323,4 +1318,4 @@ def initialize(self): def exit_sweep(self): msmt = running_measurement() msmt.step_out(reduce_dimension=True) - raise StopIteration \ No newline at end of file + raise StopIteration diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 49d666f6976..d18f095e97c 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -1,206 +1,185 @@ -import numpy as np -import shutil -import pytest import contextlib +import shutil import tempfile from pathlib import Path -from qcodes import Parameter, ManualParameter -from qcodes.utils.dataset.doNd import LinSweep -from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep +import numpy as np +import pytest + +from qcodes import ManualParameter, Parameter from qcodes.dataset import ( initialise_or_create_database_at, load_by_run_spec, load_or_create_experiment, ) +from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep +from qcodes.utils.dataset.doNd import LinSweep # def get_data_array(dataset, label): - @pytest.fixture def create_dummy_database(): + @contextlib.contextmanager + def func_context_manager(): + with tempfile.TemporaryDirectory() as temporary_folder: + temporary_folder = tempfile.TemporaryDirectory() + print(f"Created temporary folder for database: {temporary_folder}") - @contextlib.contextmanager - def func_context_manager(): - with tempfile.TemporaryDirectory() as temporary_folder: - temporary_folder = tempfile.TemporaryDirectory() - print(f'Created temporary folder for database: {temporary_folder}') + assert Path(temporary_folder.name).exists() + db_path = Path(temporary_folder.name) / "test_database.db" + initialise_or_create_database_at(str(db_path)) - assert Path(temporary_folder.name).exists() - db_path = Path(temporary_folder.name) / 'test_database.db' - initialise_or_create_database_at(str(db_path)) + yield load_or_create_experiment("test_experiment") - yield load_or_create_experiment("test_experiment") - return func_context_manager + return func_context_manager def test_create_measurement(create_dummy_database): - with create_dummy_database(): - MeasurementLoop('test') + with create_dummy_database(): + MeasurementLoop("test") def test_basic_1D_measurement(create_dummy_database): - with create_dummy_database(): - # Initialize parameters - p1_get = ManualParameter('p1_get') - p1_set = ManualParameter('p1_set') - - with MeasurementLoop('test') as msmt: - for val in Sweep(LinSweep(p1_set, 0, 1, 11)): - assert p1_set() == val - p1_get(val+1) - msmt.measure(p1_get) - - data = msmt.dataset - assert data.name == 'test' - assert data.parameters == 'p1_set,p1_get' - - arrays = data.get_parameter_data() - data_arrays = arrays['p1_get'] - - assert np.allclose( - data_arrays['p1_get'], - np.linspace(1, 2, 11) - ) - assert np.allclose( - data_arrays['p1_set'], - np.linspace(0, 1, 11) - ) - - + with create_dummy_database(): + # Initialize parameters + p1_get = ManualParameter("p1_get") + p1_set = ManualParameter("p1_set") + + with MeasurementLoop("test") as msmt: + for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + assert p1_set() == val + p1_get(val + 1) + msmt.measure(p1_get) + + data = msmt.dataset + assert data.name == "test" + assert data.parameters == "p1_set,p1_get" + + arrays = data.get_parameter_data() + data_arrays = arrays["p1_get"] + + assert np.allclose(data_arrays["p1_get"], np.linspace(1, 2, 11)) + assert np.allclose(data_arrays["p1_set"], np.linspace(0, 1, 11)) + + def test_basic_2D_measurement(create_dummy_database): - with create_dummy_database(): - # Initialize parameters - p1_get = ManualParameter('p1_get') - p1_set = ManualParameter('p1_set') - p2_set = ManualParameter('p2_set') - - with MeasurementLoop('test') as msmt: - for val in Sweep(LinSweep(p1_set, 0, 1, 11)): - assert p1_set() == val - for val2 in Sweep(LinSweep(p2_set, 0, 1, 11)): - assert p2_set() == val2 - p1_get(val+1) - msmt.measure(p1_get) - - data = msmt.dataset - assert data.name == 'test' - assert data.parameters == 'p1_set,p2_set,p1_get' - - arrays = data.get_parameter_data() - data_array = arrays['p1_get']['p1_get'] - - assert np.allclose( - data_array, - np.tile(np.linspace(1, 2, 11), (11,1)).transpose() - ) - - assert np.allclose( - arrays['p1_get']['p1_set'], - np.tile(np.linspace(0, 1, 11), (11,1)).transpose() - ) - - assert np.allclose( - arrays['p1_get']['p2_set'], - np.tile(np.linspace(0, 1, 11), (11,1)) - ) - + with create_dummy_database(): + # Initialize parameters + p1_get = ManualParameter("p1_get") + p1_set = ManualParameter("p1_set") + p2_set = ManualParameter("p2_set") + + with MeasurementLoop("test") as msmt: + for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + assert p1_set() == val + for val2 in Sweep(LinSweep(p2_set, 0, 1, 11)): + assert p2_set() == val2 + p1_get(val + 1) + msmt.measure(p1_get) + + data = msmt.dataset + assert data.name == "test" + assert data.parameters == "p1_set,p2_set,p1_get" + + arrays = data.get_parameter_data() + data_array = arrays["p1_get"]["p1_get"] + + assert np.allclose(data_array, np.tile(np.linspace(1, 2, 11), (11, 1)).transpose()) + + assert np.allclose( + arrays["p1_get"]["p1_set"], np.tile(np.linspace(0, 1, 11), (11, 1)).transpose() + ) + + assert np.allclose( + arrays["p1_get"]["p2_set"], np.tile(np.linspace(0, 1, 11), (11, 1)) + ) + def test_1D_measurement_duplicate_get(create_dummy_database): - with create_dummy_database(): - # Initialize parameters - p1_get = ManualParameter('p1_get') - p1_set = ManualParameter('p1_set') - - with MeasurementLoop('test') as msmt: - for val in Sweep(LinSweep(p1_set, 0, 1, 11)): - assert p1_set() == val - p1_get(val+1) - msmt.measure(p1_get) - p1_get(val+0.5) - msmt.measure(p1_get) - - data = msmt.dataset - assert data.name == 'test' - assert data.parameters == 'p1_set,p1_get_0,p1_get_1' - - arrays = data.get_parameter_data() - - offsets = {'p1_get_0': 1, 'p1_get_1': 0.5} - for key in ['p1_get_0', 'p1_get_1']: - data_arrays = arrays[key] - - assert np.allclose( - data_arrays[key], - np.linspace(0, 1, 11) + offsets[key] - ) - assert np.allclose( - data_arrays['p1_set'], - np.linspace(0, 1, 11) - ) + with create_dummy_database(): + # Initialize parameters + p1_get = ManualParameter("p1_get") + p1_set = ManualParameter("p1_set") + + with MeasurementLoop("test") as msmt: + for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + assert p1_set() == val + p1_get(val + 1) + msmt.measure(p1_get) + p1_get(val + 0.5) + msmt.measure(p1_get) + + data = msmt.dataset + assert data.name == "test" + assert data.parameters == "p1_set,p1_get_0,p1_get_1" + + arrays = data.get_parameter_data() + + offsets = {"p1_get_0": 1, "p1_get_1": 0.5} + for key in ["p1_get_0", "p1_get_1"]: + data_arrays = arrays[key] + + assert np.allclose(data_arrays[key], np.linspace(0, 1, 11) + offsets[key]) + assert np.allclose(data_arrays["p1_set"], np.linspace(0, 1, 11)) def test_1D_measurement_duplicate_getset(create_dummy_database): - with create_dummy_database(): - # Initialize parameters - p1_get = ManualParameter('p1_get') - p1_set = ManualParameter('p1_set') - - with MeasurementLoop('test') as msmt: - for val in Sweep(LinSweep(p1_set, 0, 1, 11)): - assert p1_set() == val - p1_get(val+1) - msmt.measure(p1_get) - for val in Sweep(LinSweep(p1_set, 0, 1, 11)): - assert p1_set() == val - p1_get(val+0.5) - msmt.measure(p1_get) - - data = msmt.dataset - assert data.name == 'test' - assert data.parameters == 'p1_set_0,p1_set_1,p1_get_0,p1_get_1' - - arrays = data.get_parameter_data() - - offsets = {'p1_get_0': 1, 'p1_get_1': 0.5} - for k in [0, 1]: - get_key = f'p1_get_{k}' - set_key = f'p1_set_{k}' - data_arrays = arrays[get_key] - - assert np.allclose( - data_arrays[get_key], - np.linspace(0, 1, 11) + offsets[get_key] - ) - assert np.allclose( - data_arrays[set_key], - np.linspace(0, 1, 11) - ) - - + with create_dummy_database(): + # Initialize parameters + p1_get = ManualParameter("p1_get") + p1_set = ManualParameter("p1_set") + + with MeasurementLoop("test") as msmt: + for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + assert p1_set() == val + p1_get(val + 1) + msmt.measure(p1_get) + for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + assert p1_set() == val + p1_get(val + 0.5) + msmt.measure(p1_get) + + data = msmt.dataset + assert data.name == "test" + assert data.parameters == "p1_set_0,p1_set_1,p1_get_0,p1_get_1" + + arrays = data.get_parameter_data() + + offsets = {"p1_get_0": 1, "p1_get_1": 0.5} + for k in [0, 1]: + get_key = f"p1_get_{k}" + set_key = f"p1_set_{k}" + data_arrays = arrays[get_key] + + assert np.allclose( + data_arrays[get_key], np.linspace(0, 1, 11) + offsets[get_key] + ) + assert np.allclose(data_arrays[set_key], np.linspace(0, 1, 11)) + + def test_2D_measurement_initialization(create_dummy_database): - with create_dummy_database(): - # Initialize parameters - p1_get = ManualParameter('p1_get') - p1_set = ManualParameter('p1_set') - p2_set = ManualParameter('p2_set') - - with MeasurementLoop('test') as msmt: - outer_sweep = Sweep(LinSweep(p1_set, 0, 1, 11)) - for k, val in enumerate(outer_sweep): - assert p1_set() == val - assert outer_sweep.is_first_sweep - - inner_sweep = Sweep(LinSweep(p2_set, 0, 1, 11)) - assert not inner_sweep.is_first_sweep - - for val2 in inner_sweep: - assert p2_set() == val2 - p1_get(val+1) - msmt.measure(p1_get) - - if not k: - assert not msmt.data_handler.initialized - else: - assert msmt.data_handler.initialized \ No newline at end of file + with create_dummy_database(): + # Initialize parameters + p1_get = ManualParameter("p1_get") + p1_set = ManualParameter("p1_set") + p2_set = ManualParameter("p2_set") + + with MeasurementLoop("test") as msmt: + outer_sweep = Sweep(LinSweep(p1_set, 0, 1, 11)) + for k, val in enumerate(outer_sweep): + assert p1_set() == val + assert outer_sweep.is_first_sweep + + inner_sweep = Sweep(LinSweep(p2_set, 0, 1, 11)) + assert not inner_sweep.is_first_sweep + + for val2 in inner_sweep: + assert p2_set() == val2 + p1_get(val + 1) + msmt.measure(p1_get) + + if not k: + assert not msmt.data_handler.initialized + else: + assert msmt.data_handler.initialized diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index 9b0db108684..14552fabc6c 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -1,6 +1,4 @@ import builtins -import sys -import pprint import collections import io import json @@ -8,6 +6,8 @@ import math import numbers import os +import pprint +import sys import time import warnings from asyncio import iscoroutinefunction @@ -901,4 +901,4 @@ def record(self, key, val=None): # Optionally remove oldest elements for _ in range(len(timing_list) - self.max_records): - timing_list.pop(0) \ No newline at end of file + timing_list.pop(0) From 47beddd62e2731fc3eac6d34c1b30f30c23816fc Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 5 Apr 2022 08:29:42 +0200 Subject: [PATCH 015/122] merge --- .../instrument_drivers/Keysight/__init__.py | 0 .../instrument_drivers/keysight/__init__.py | 0 .../test_measurement_loop_basics.py | 154 +++++++++++++++--- 3 files changed, 129 insertions(+), 25 deletions(-) delete mode 100644 qcodes/instrument_drivers/Keysight/__init__.py delete mode 100644 qcodes/instrument_drivers/keysight/__init__.py diff --git a/qcodes/instrument_drivers/Keysight/__init__.py b/qcodes/instrument_drivers/Keysight/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/qcodes/instrument_drivers/keysight/__init__.py b/qcodes/instrument_drivers/keysight/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index d18f095e97c..430c410350f 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -7,14 +7,20 @@ import pytest from qcodes import ManualParameter, Parameter +from qcodes.dataset.data_set import load_by_id +from qcodes.dataset.descriptions.rundescriber import RunDescriber +from qcodes.utils.dataset.doNd import LinSweep +from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep from qcodes.dataset import ( initialise_or_create_database_at, - load_by_run_spec, load_or_create_experiment, ) from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep from qcodes.utils.dataset.doNd import LinSweep +from qcodes.dataset.descriptions.versioning.converters import new_to_old +from qcodes.dataset.descriptions.versioning import serialization as serial +from qcodes.dataset.sqlite.queries import update_run_description, add_parameter # def get_data_array(dataset, label): @@ -159,27 +165,125 @@ def test_1D_measurement_duplicate_getset(create_dummy_database): def test_2D_measurement_initialization(create_dummy_database): - with create_dummy_database(): - # Initialize parameters - p1_get = ManualParameter("p1_get") - p1_set = ManualParameter("p1_set") - p2_set = ManualParameter("p2_set") - - with MeasurementLoop("test") as msmt: - outer_sweep = Sweep(LinSweep(p1_set, 0, 1, 11)) - for k, val in enumerate(outer_sweep): - assert p1_set() == val - assert outer_sweep.is_first_sweep - - inner_sweep = Sweep(LinSweep(p2_set, 0, 1, 11)) - assert not inner_sweep.is_first_sweep - - for val2 in inner_sweep: - assert p2_set() == val2 - p1_get(val + 1) - msmt.measure(p1_get) - - if not k: - assert not msmt.data_handler.initialized - else: - assert msmt.data_handler.initialized + with create_dummy_database(): + # Initialize parameters + p1_get = ManualParameter('p1_get') + p1_set = ManualParameter('p1_set') + p2_set = ManualParameter('p2_set') + + with MeasurementLoop('test') as msmt: + outer_sweep = Sweep(LinSweep(p1_set, 0, 1, 11)) + for k, val in enumerate(outer_sweep): + assert p1_set() == val + assert outer_sweep.is_first_sweep + + inner_sweep = Sweep(LinSweep(p2_set, 0, 1, 11)) + assert not inner_sweep.is_first_sweep + + for val2 in inner_sweep: + assert p2_set() == val2 + p1_get(val+1) + msmt.measure(p1_get) + + if not k: + assert not msmt.data_handler.initialized + else: + assert msmt.data_handler.initialized + +def update_interdependencies(msmt, datasaver): + dataset = datasaver.dataset + + # Get previous paramspecs + previous_paramspecs = dataset._rundescriber.interdeps.paramspecs + previous_paramspec_names = [spec.name for spec in previous_paramspecs] + + # Update DataSaver + datasaver._interdeps = msmt._interdeps + + # Generate new paramspecs with matching RunDescriber + dataset._rundescriber = RunDescriber(msmt._interdeps, shapes=msmt._shapes) + paramspecs = new_to_old(dataset._rundescriber.interdeps).paramspecs + + # Add new paramspecs + for spec in paramspecs: + if spec.name not in previous_paramspec_names: + add_parameter( + spec, conn=dataset.conn, run_id=dataset.run_id, + insert_into_results_table=True + ) + + desc_str = serial.to_json_for_storage(dataset.description) + + update_run_description(dataset.conn, dataset.run_id, desc_str) + + +def test_dataset_registering_shared_set(create_dummy_database): + from qcodes import Measurement + + with create_dummy_database(): + p1_get = ManualParameter('p1_get') + p1_set = ManualParameter('p1_set') + + p2_get = ManualParameter('p2_get') + + msmt = Measurement() + msmt.register_parameter(p1_set) + msmt.register_parameter(p1_get, setpoints=(p1_set,)) + # TODO allow cache + with msmt.run(in_memory_cache=False) as datasaver: + dataset = datasaver.dataset + + for k, set_v in enumerate(np.linspace(0, 25, 10)): + p1_set(set_v) + datasaver.add_result((p1_set, set_v), + (p1_get, 123)) + if not k: + msmt.register_parameter(p2_get, setpoints=(p1_set,)) + update_interdependencies(msmt, datasaver) + + datasaver.add_result((p1_set, set_v), + (p2_get, 124)) + + loaded_dataset = load_by_id(dataset.run_id) + run_description = loaded_dataset._get_run_description_from_db() + print(run_description) + + +def test_dataset_registering_separate_set(create_dummy_database): + from qcodes import Measurement + + with create_dummy_database(): + p1_get = ManualParameter('p1_get') + p1_set = ManualParameter('p1_set') + + p2_set = ManualParameter('p2_set') + p2_get = ManualParameter('p2_get') + + msmt = Measurement() + msmt.register_parameter(p1_set) + msmt.register_parameter(p1_get, setpoints=(p1_set,)) + # TODO allow cache + with msmt.run(in_memory_cache=False) as datasaver: + dataset = datasaver.dataset + + for set_v in np.linspace(0, 25, 10): + p1_set(set_v) + datasaver.add_result((p1_set, set_v), + (p1_get, 123)) + + + # Add new parameters + msmt.register_parameter(p2_set) + msmt.register_parameter(p2_get, setpoints=(p2_set,)) + update_interdependencies(msmt, datasaver) + + print(msmt._interdeps) + for set_v in np.linspace(0, 25, 10): + p2_set(set_v) + datasaver.add_result((p2_set, set_v), + (p2_get, 124)) + + + loaded_dataset = load_by_id(dataset.run_id) + run_description = loaded_dataset._get_run_description_from_db() + print(run_description) From f7e7a4d90bbea8243c1d84858d299ba6767dbf7e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Apr 2022 06:31:00 +0000 Subject: [PATCH 016/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../test_measurement_loop_basics.py | 226 +++++++++--------- 1 file changed, 109 insertions(+), 117 deletions(-) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 430c410350f..3f64f40b3ec 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -7,20 +7,15 @@ import pytest from qcodes import ManualParameter, Parameter +from qcodes.dataset import initialise_or_create_database_at, load_or_create_experiment from qcodes.dataset.data_set import load_by_id from qcodes.dataset.descriptions.rundescriber import RunDescriber -from qcodes.utils.dataset.doNd import LinSweep -from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep -from qcodes.dataset import ( - initialise_or_create_database_at, - load_or_create_experiment, -) +from qcodes.dataset.descriptions.versioning import serialization as serial +from qcodes.dataset.descriptions.versioning.converters import new_to_old from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep +from qcodes.dataset.sqlite.queries import add_parameter, update_run_description from qcodes.utils.dataset.doNd import LinSweep -from qcodes.dataset.descriptions.versioning.converters import new_to_old -from qcodes.dataset.descriptions.versioning import serialization as serial -from qcodes.dataset.sqlite.queries import update_run_description, add_parameter # def get_data_array(dataset, label): @@ -165,125 +160,122 @@ def test_1D_measurement_duplicate_getset(create_dummy_database): def test_2D_measurement_initialization(create_dummy_database): - with create_dummy_database(): - # Initialize parameters - p1_get = ManualParameter('p1_get') - p1_set = ManualParameter('p1_set') - p2_set = ManualParameter('p2_set') - - with MeasurementLoop('test') as msmt: - outer_sweep = Sweep(LinSweep(p1_set, 0, 1, 11)) - for k, val in enumerate(outer_sweep): - assert p1_set() == val - assert outer_sweep.is_first_sweep - - inner_sweep = Sweep(LinSweep(p2_set, 0, 1, 11)) - assert not inner_sweep.is_first_sweep - - for val2 in inner_sweep: - assert p2_set() == val2 - p1_get(val+1) - msmt.measure(p1_get) - - if not k: - assert not msmt.data_handler.initialized - else: - assert msmt.data_handler.initialized + with create_dummy_database(): + # Initialize parameters + p1_get = ManualParameter("p1_get") + p1_set = ManualParameter("p1_set") + p2_set = ManualParameter("p2_set") + + with MeasurementLoop("test") as msmt: + outer_sweep = Sweep(LinSweep(p1_set, 0, 1, 11)) + for k, val in enumerate(outer_sweep): + assert p1_set() == val + assert outer_sweep.is_first_sweep + + inner_sweep = Sweep(LinSweep(p2_set, 0, 1, 11)) + assert not inner_sweep.is_first_sweep + + for val2 in inner_sweep: + assert p2_set() == val2 + p1_get(val + 1) + msmt.measure(p1_get) + + if not k: + assert not msmt.data_handler.initialized + else: + assert msmt.data_handler.initialized + def update_interdependencies(msmt, datasaver): - dataset = datasaver.dataset + dataset = datasaver.dataset - # Get previous paramspecs - previous_paramspecs = dataset._rundescriber.interdeps.paramspecs - previous_paramspec_names = [spec.name for spec in previous_paramspecs] + # Get previous paramspecs + previous_paramspecs = dataset._rundescriber.interdeps.paramspecs + previous_paramspec_names = [spec.name for spec in previous_paramspecs] - # Update DataSaver - datasaver._interdeps = msmt._interdeps + # Update DataSaver + datasaver._interdeps = msmt._interdeps - # Generate new paramspecs with matching RunDescriber - dataset._rundescriber = RunDescriber(msmt._interdeps, shapes=msmt._shapes) - paramspecs = new_to_old(dataset._rundescriber.interdeps).paramspecs + # Generate new paramspecs with matching RunDescriber + dataset._rundescriber = RunDescriber(msmt._interdeps, shapes=msmt._shapes) + paramspecs = new_to_old(dataset._rundescriber.interdeps).paramspecs - # Add new paramspecs - for spec in paramspecs: - if spec.name not in previous_paramspec_names: - add_parameter( - spec, conn=dataset.conn, run_id=dataset.run_id, - insert_into_results_table=True - ) + # Add new paramspecs + for spec in paramspecs: + if spec.name not in previous_paramspec_names: + add_parameter( + spec, + conn=dataset.conn, + run_id=dataset.run_id, + insert_into_results_table=True, + ) - desc_str = serial.to_json_for_storage(dataset.description) + desc_str = serial.to_json_for_storage(dataset.description) - update_run_description(dataset.conn, dataset.run_id, desc_str) + update_run_description(dataset.conn, dataset.run_id, desc_str) def test_dataset_registering_shared_set(create_dummy_database): - from qcodes import Measurement - - with create_dummy_database(): - p1_get = ManualParameter('p1_get') - p1_set = ManualParameter('p1_set') - - p2_get = ManualParameter('p2_get') - - msmt = Measurement() - msmt.register_parameter(p1_set) - msmt.register_parameter(p1_get, setpoints=(p1_set,)) - # TODO allow cache - with msmt.run(in_memory_cache=False) as datasaver: - dataset = datasaver.dataset - - for k, set_v in enumerate(np.linspace(0, 25, 10)): - p1_set(set_v) - datasaver.add_result((p1_set, set_v), - (p1_get, 123)) - if not k: - msmt.register_parameter(p2_get, setpoints=(p1_set,)) - update_interdependencies(msmt, datasaver) - - datasaver.add_result((p1_set, set_v), - (p2_get, 124)) - - loaded_dataset = load_by_id(dataset.run_id) - run_description = loaded_dataset._get_run_description_from_db() - print(run_description) + from qcodes import Measurement + + with create_dummy_database(): + p1_get = ManualParameter("p1_get") + p1_set = ManualParameter("p1_set") + + p2_get = ManualParameter("p2_get") + + msmt = Measurement() + msmt.register_parameter(p1_set) + msmt.register_parameter(p1_get, setpoints=(p1_set,)) + # TODO allow cache + with msmt.run(in_memory_cache=False) as datasaver: + dataset = datasaver.dataset + + for k, set_v in enumerate(np.linspace(0, 25, 10)): + p1_set(set_v) + datasaver.add_result((p1_set, set_v), (p1_get, 123)) + if not k: + msmt.register_parameter(p2_get, setpoints=(p1_set,)) + update_interdependencies(msmt, datasaver) + + datasaver.add_result((p1_set, set_v), (p2_get, 124)) + + loaded_dataset = load_by_id(dataset.run_id) + run_description = loaded_dataset._get_run_description_from_db() + print(run_description) def test_dataset_registering_separate_set(create_dummy_database): - from qcodes import Measurement - - with create_dummy_database(): - p1_get = ManualParameter('p1_get') - p1_set = ManualParameter('p1_set') - - p2_set = ManualParameter('p2_set') - p2_get = ManualParameter('p2_get') - - msmt = Measurement() - msmt.register_parameter(p1_set) - msmt.register_parameter(p1_get, setpoints=(p1_set,)) - # TODO allow cache - with msmt.run(in_memory_cache=False) as datasaver: - dataset = datasaver.dataset - - for set_v in np.linspace(0, 25, 10): - p1_set(set_v) - datasaver.add_result((p1_set, set_v), - (p1_get, 123)) - - - # Add new parameters - msmt.register_parameter(p2_set) - msmt.register_parameter(p2_get, setpoints=(p2_set,)) - update_interdependencies(msmt, datasaver) - - print(msmt._interdeps) - for set_v in np.linspace(0, 25, 10): - p2_set(set_v) - datasaver.add_result((p2_set, set_v), - (p2_get, 124)) - - - loaded_dataset = load_by_id(dataset.run_id) - run_description = loaded_dataset._get_run_description_from_db() - print(run_description) + from qcodes import Measurement + + with create_dummy_database(): + p1_get = ManualParameter("p1_get") + p1_set = ManualParameter("p1_set") + + p2_set = ManualParameter("p2_set") + p2_get = ManualParameter("p2_get") + + msmt = Measurement() + msmt.register_parameter(p1_set) + msmt.register_parameter(p1_get, setpoints=(p1_set,)) + # TODO allow cache + with msmt.run(in_memory_cache=False) as datasaver: + dataset = datasaver.dataset + + for set_v in np.linspace(0, 25, 10): + p1_set(set_v) + datasaver.add_result((p1_set, set_v), (p1_get, 123)) + + # Add new parameters + msmt.register_parameter(p2_set) + msmt.register_parameter(p2_get, setpoints=(p2_set,)) + update_interdependencies(msmt, datasaver) + + print(msmt._interdeps) + for set_v in np.linspace(0, 25, 10): + p2_set(set_v) + datasaver.add_result((p2_set, set_v), (p2_get, 124)) + + loaded_dataset = load_by_id(dataset.run_id) + run_description = loaded_dataset._get_run_description_from_db() + print(run_description) From 7e80e47fa49728e9b2d79f0d643c8407d7ff9575 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 5 Apr 2022 21:13:11 +0200 Subject: [PATCH 017/122] Implemented incremental adding of parameters --- qcodes/dataset/data_set.py | 3 +- qcodes/dataset/measurement_loop.py | 228 +++++++++++------- qcodes/dataset/measurements.py | 5 + .../test_measurement_loop_basics.py | 75 ++---- 4 files changed, 167 insertions(+), 144 deletions(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index 3d2a8b66b34..c005d4a9788 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -318,11 +318,12 @@ def prepare( shapes: Shapes = None, parent_datasets: Sequence[Mapping[Any, Any]] = (), write_in_background: bool = False, + allow_empty_dataset: bool = False ) -> None: self.add_snapshot(json.dumps({"station": snapshot}, cls=NumpyJSONEncoder)) - if interdeps == InterDependencies_(): + if interdeps == InterDependencies_() and not allow_empty_dataset: raise RuntimeError("No parameters supplied") self.set_interdependencies(interdeps, shapes) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 8efdf1d7d4a..82032d635fd 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -24,6 +24,10 @@ get_last_input_cells, using_ipython, ) +from qcodes.dataset.descriptions.rundescriber import RunDescriber +from qcodes.dataset.descriptions.versioning import serialization as serial +from qcodes.dataset.descriptions.versioning.converters import new_to_old +from qcodes.dataset.sqlite.queries import add_parameter, update_run_description RAW_VALUE_TYPES = (float, int, bool, np.ndarray, np.integer, np.floating, np.bool_, type(None)) @@ -56,95 +60,67 @@ def __init__(self, measurement_loop, name='results'): # - unstored_results - list where each element contains (*setpoints, measurement_value) # - latest_value - # TODO should be called at appropriate time + self.initialize() + def initialize(self): # Once initialized, no new parameters can be added assert not self.initialized, "Cannot initialize twice" + # Create Measurement self.measurement = Measurement(name=self.name) - # Register all setpoints parameters - self._create_unique_dataset_parameters(self.setpoint_list) - for setpoint_info in self.setpoint_list.values(): - self.measurement.register_parameter(setpoint_info['dataset_parameter']) - - # Determine setpoint_parameters for each measurement_parameter - for measurement_info in self.measurement_list.values(): - measurement_info['setpoint_parameters'] = tuple( - self.setpoint_list[action_indices]['dataset_parameter'] - for action_indices in measurement_info['setpoints_action_indices'] - ) - - # Register all measurement parameters - self._create_unique_dataset_parameters(self.measurement_list) - for measurement_info in self.measurement_list.values(): - self.measurement.register_parameter( - measurement_info['dataset_parameter'], - setpoints=measurement_info['setpoint_parameters'] - ) - self.measurement.set_shapes( - detect_shape_of_measurement( - (measurement_info["dataset_parameter"],), measurement_info["shape"] - ) - ) - # Create measurement Runner - self.runner = self.measurement.run() + # TODO remove cache + self.runner = self.measurement.run( + allow_empty_dataset=True, + in_memory_cache=False + ) # Create measurement Dataset self.datasaver = self.runner.__enter__() self.dataset = self.datasaver.dataset - # Add results that were taken before initializing dataset - for measurement_info in self.measurement_list.values(): - for unstored_result in measurement_info['unstored_results']: - parameters = *measurement_info['setpoint_parameters'], measurement_info['dataset_parameter'] - result = tuple(zip(parameters, unstored_result)) - self.datasaver.add_result(*result) - self.initialized = True def finalize(self): - if not self.initialized: - self.initialize() - self.datasaver.flush_data_to_database() - def _create_unique_dataset_parameters(self, parameter_list): - """Populates 'dataset_parameter' of parameter_list + def _ensure_unique_parameter(self, parameter_info, setpoint, max_idx=99): + """Ensure parameters have unique names""" + if setpoint: + parameter_list = self.setpoint_list + else: + parameter_list = self.measurement_list - Ensure parameters have unique names - """ - parameter_names = [param_info['parameter'].name for param_info in parameter_list.values()] - duplicate_names = [name for name, count in Counter(parameter_names).items() if count > 1] - unique_names = [name for name, count in Counter(parameter_names).items() if count == 1] - - for name in unique_names: - parameter_info = next( - param_info for param_info in parameter_list.values() - if param_info['parameter'].name == name - ) - parameter_info['dataset_parameter'] = parameter_info['parameter'] + parameter_names = [ + param_info['dataset_parameter'].name + for param_info in parameter_list.values() + if 'dataset_parameter' in param_info + ] - for name in duplicate_names: - # Need to rename parameters with duplicate names - duplicate_parameter_info_list = [ - param_info for param_info in parameter_list.values() - if param_info['parameter'].name == name - ] - for k, parameter_info in enumerate(duplicate_parameter_info_list): - # Create delegate parameter - delegate_parameter = DelegateParameter( - name=f"{parameter_info['parameter'].name}_{k}", - source=parameter_info['parameter'] - ) - parameter_info['dataset_parameter'] = delegate_parameter + parameter_name = parameter_info['parameter'].name + if parameter_name not in parameter_names: + parameter_info['dataset_parameter'] = parameter_info['parameter'] + else: + for idx in range(1, max_idx): + parameter_idx_name = f'{parameter_name}_{idx}' + if parameter_idx_name not in parameter_names: + parameter_name = parameter_idx_name + break + else: + raise OverflowError( + f'All parameter names {parameter_name}_{{idx}} up to idx {max_idx} are taken' + ) + # Create a delegate parameter with modified name + delegate_parameter = DelegateParameter( + name=parameter_name, + source=parameter_info['parameter'] + ) + parameter_info['dataset_parameter'] = delegate_parameter def create_measurement_info( self, action_indices, parameter, name=None, label=None, unit=None ): - assert not self.initialized, "Cannot measure parameter for the first time after initializing dataset" - if parameter is None: assert name is not None parameter = Parameter(name=name, label=label, unit=unit) @@ -169,11 +145,32 @@ def create_measurement_info( 'parameter': parameter, 'setpoints_action_indices': setpoints_action_indices, 'shape': self.measurement_loop.loop_shape, - 'unstored_results': [] + 'unstored_results': [], + 'registered': False } return measurement_info + def register_new_measurement( + self, + action_indices, + parameter, + name: str = None, + label: str = None, + unit: str = None + ): + measurement_info = self.create_measurement_info( + action_indices=action_indices, + parameter=parameter, + name=name, + label=label, + unit=unit + ) + self.measurement_list[action_indices] = measurement_info + + # Add new measurement parameter + self._update_interdependencies() + def add_measurement_result( self, action_indices, @@ -196,14 +193,13 @@ def add_measurement_result( # Get parameter data array, creating a new one if necessary if action_indices not in self.measurement_list: - measurement_info = self.create_measurement_info( + self.register_new_measurement( action_indices=action_indices, parameter=parameter, name=name, label=label, unit=unit ) - self.measurement_list[action_indices] = measurement_info measurement_info = self.measurement_list[action_indices] @@ -220,20 +216,81 @@ def add_measurement_result( self.setpoint_list[action_indices]['latest_value'] for action_indices in measurement_info['setpoints_action_indices'] ] + parameters = ( + *measurement_info["setpoint_parameters"], + measurement_info["dataset_parameter"], + ) + result_with_setpoints = tuple(zip(parameters, (*setpoints, result))) + self.datasaver.add_result(*result_with_setpoints) - if self.initialized: - parameters = ( - *measurement_info["setpoint_parameters"], - measurement_info["dataset_parameter"], - ) - - result_with_setpoints = tuple(zip(parameters, (*setpoints, result))) - self.datasaver.add_result(*result_with_setpoints) - else: - measurement_info['unstored_results'].append((*setpoints, result)) # Also store in measurement_info measurement_info["latest_value"] = result + def _update_interdependencies(self): + dataset = self.datasaver.dataset + + # Get previous paramspecs + previous_paramspecs = dataset._rundescriber.interdeps.paramspecs + previous_paramspec_names = [spec.name for spec in previous_paramspecs] + + # Register all new setpoints parameters in Measurement + for setpoint_info in self.setpoint_list.values(): + if setpoint_info['registered']: + # Already registered + continue + + self._ensure_unique_parameter(setpoint_info, setpoint=True) + self.measurement.register_parameter(setpoint_info['dataset_parameter']) + setpoint_info['registered'] = True + + # Register all measurement parameters in Measurement + for measurement_info in self.measurement_list.values(): + if measurement_info['registered']: + # Already registered + continue + + # Determine setpoint_parameters for each measurement_parameter + for measurement_info in self.measurement_list.values(): + measurement_info['setpoint_parameters'] = tuple( + self.setpoint_list[action_indices]['dataset_parameter'] + for action_indices in measurement_info['setpoints_action_indices'] + ) + + self._ensure_unique_parameter(measurement_info, setpoint=False) + self.measurement.register_parameter( + measurement_info['dataset_parameter'], + setpoints=measurement_info['setpoint_parameters'] + ) + measurement_info['registered'] = True + self.measurement.set_shapes( + detect_shape_of_measurement( + (measurement_info["dataset_parameter"],), measurement_info["shape"] + ) + ) + + # Update DataSaver + self.datasaver._interdeps = self.measurement._interdeps + + # Generate new paramspecs with matching RunDescriber + dataset._rundescriber = RunDescriber( + self.measurement._interdeps, + shapes=self.measurement._shapes + ) + paramspecs = new_to_old(dataset._rundescriber.interdeps).paramspecs + + # Add new paramspecs + for spec in paramspecs: + if spec.name not in previous_paramspec_names: + add_parameter( + spec, + conn=dataset.conn, + run_id=dataset.run_id, + insert_into_results_table=True, + ) + + desc_str = serial.to_json_for_storage(dataset.description) + + update_run_description(dataset.conn, dataset.run_id, desc_str) class MeasurementLoop: """Class to perform measurements @@ -1200,7 +1257,6 @@ def __init__(self, sequence, name=None, label=None, unit=None, reverse=False, re # Check if this is the first sweep # Useful to know when to initialize dataset msmt = running_measurement() - self.is_first_sweep = not any(isinstance(action, Sweep) for action in msmt.actions.values()) # Create setpoint_list self.initialize() @@ -1242,15 +1298,6 @@ def __next__(self): elif msmt.is_stopped: raise SystemExit - # Initialize data handler if the first sweep reaches its second iteration - # would be nicer if the sweep doesn't talk to the data handler - if self.is_first_sweep: - if (self.reverse and self.loop_index == len(self.sequence) - 2) or ( - not self.reverse and self.loop_index == 1 - ): - if not msmt.data_handler.initialized: - msmt.data_handler.initialize() - # Wait as long as the measurement is paused while msmt.is_paused: sleep(0.1) @@ -1303,7 +1350,8 @@ def initialize(self): setpoint_info = { 'parameter': set_parameter, - 'latest_value': None + 'latest_value': None, + 'registered': False } # Add to setpoint list diff --git a/qcodes/dataset/measurements.py b/qcodes/dataset/measurements.py index 5a522634ccd..3894bdebf2f 100644 --- a/qcodes/dataset/measurements.py +++ b/qcodes/dataset/measurements.py @@ -505,6 +505,7 @@ def __init__( shapes: Optional[Shapes] = None, in_memory_cache: bool = True, dataset_class: DataSetType = DataSetType.DataSet, + allow_empty_dataset: bool = False ) -> None: self._dataset_class = dataset_class @@ -527,6 +528,7 @@ def __init__( self._extra_log_info = extra_log_info self._write_in_background = write_in_background self._in_memory_cache = in_memory_cache + self.allow_empty_dataset = allow_empty_dataset self.ds: DataSetProtocol @staticmethod @@ -604,6 +606,7 @@ def __enter__(self) -> DataSaver: write_in_background=self._write_in_background, shapes=self._shapes, parent_datasets=self._parent_datasets, + allow_empty_dataset=self.allow_empty_dataset ) # register all subscribers @@ -1208,6 +1211,7 @@ def run( write_in_background: Optional[bool] = None, in_memory_cache: bool = True, dataset_class: DataSetType = DataSetType.DataSet, + allow_empty_dataset: bool = False ) -> Runner: """ Returns the context manager for the experimental run @@ -1241,4 +1245,5 @@ def run( shapes=self._shapes, in_memory_cache=in_memory_cache, dataset_class=dataset_class, + allow_empty_dataset=allow_empty_dataset ) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 3f64f40b3ec..8153d395a82 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -9,11 +9,7 @@ from qcodes import ManualParameter, Parameter from qcodes.dataset import initialise_or_create_database_at, load_or_create_experiment from qcodes.dataset.data_set import load_by_id -from qcodes.dataset.descriptions.rundescriber import RunDescriber -from qcodes.dataset.descriptions.versioning import serialization as serial -from qcodes.dataset.descriptions.versioning.converters import new_to_old from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep -from qcodes.dataset.sqlite.queries import add_parameter, update_run_description from qcodes.utils.dataset.doNd import LinSweep # def get_data_array(dataset, label): @@ -113,12 +109,12 @@ def test_1D_measurement_duplicate_get(create_dummy_database): data = msmt.dataset assert data.name == "test" - assert data.parameters == "p1_set,p1_get_0,p1_get_1" + assert data.parameters == "p1_set,p1_get,p1_get_1" arrays = data.get_parameter_data() - offsets = {"p1_get_0": 1, "p1_get_1": 0.5} - for key in ["p1_get_0", "p1_get_1"]: + offsets = {"p1_get": 1, "p1_get_1": 0.5} + for key in ["p1_get", "p1_get_1"]: data_arrays = arrays[key] assert np.allclose(data_arrays[key], np.linspace(0, 1, 11) + offsets[key]) @@ -143,14 +139,14 @@ def test_1D_measurement_duplicate_getset(create_dummy_database): data = msmt.dataset assert data.name == "test" - assert data.parameters == "p1_set_0,p1_set_1,p1_get_0,p1_get_1" + assert data.parameters == "p1_set,p1_get,p1_set_1,p1_get_1" arrays = data.get_parameter_data() - offsets = {"p1_get_0": 1, "p1_get_1": 0.5} - for k in [0, 1]: - get_key = f"p1_get_{k}" - set_key = f"p1_set_{k}" + offsets = {"p1_get": 1, "p1_get_1": 0.5} + for suffix in ['', '_1']: + get_key = f"p1_get{suffix}" + set_key = f"p1_set{suffix}" data_arrays = arrays[get_key] assert np.allclose( @@ -170,50 +166,12 @@ def test_2D_measurement_initialization(create_dummy_database): outer_sweep = Sweep(LinSweep(p1_set, 0, 1, 11)) for k, val in enumerate(outer_sweep): assert p1_set() == val - assert outer_sweep.is_first_sweep - inner_sweep = Sweep(LinSweep(p2_set, 0, 1, 11)) - assert not inner_sweep.is_first_sweep - - for val2 in inner_sweep: + for val2 in Sweep(LinSweep(p2_set, 0, 1, 11)): assert p2_set() == val2 p1_get(val + 1) msmt.measure(p1_get) - if not k: - assert not msmt.data_handler.initialized - else: - assert msmt.data_handler.initialized - - -def update_interdependencies(msmt, datasaver): - dataset = datasaver.dataset - - # Get previous paramspecs - previous_paramspecs = dataset._rundescriber.interdeps.paramspecs - previous_paramspec_names = [spec.name for spec in previous_paramspecs] - - # Update DataSaver - datasaver._interdeps = msmt._interdeps - - # Generate new paramspecs with matching RunDescriber - dataset._rundescriber = RunDescriber(msmt._interdeps, shapes=msmt._shapes) - paramspecs = new_to_old(dataset._rundescriber.interdeps).paramspecs - - # Add new paramspecs - for spec in paramspecs: - if spec.name not in previous_paramspec_names: - add_parameter( - spec, - conn=dataset.conn, - run_id=dataset.run_id, - insert_into_results_table=True, - ) - - desc_str = serial.to_json_for_storage(dataset.description) - - update_run_description(dataset.conn, dataset.run_id, desc_str) - def test_dataset_registering_shared_set(create_dummy_database): from qcodes import Measurement @@ -236,7 +194,7 @@ def test_dataset_registering_shared_set(create_dummy_database): datasaver.add_result((p1_set, set_v), (p1_get, 123)) if not k: msmt.register_parameter(p2_get, setpoints=(p1_set,)) - update_interdependencies(msmt, datasaver) + # update_interdependencies(msmt, datasaver) datasaver.add_result((p1_set, set_v), (p2_get, 124)) @@ -269,7 +227,7 @@ def test_dataset_registering_separate_set(create_dummy_database): # Add new parameters msmt.register_parameter(p2_set) msmt.register_parameter(p2_get, setpoints=(p2_set,)) - update_interdependencies(msmt, datasaver) + # update_interdependencies(msmt, datasaver) print(msmt._interdeps) for set_v in np.linspace(0, 25, 10): @@ -279,3 +237,14 @@ def test_dataset_registering_separate_set(create_dummy_database): loaded_dataset = load_by_id(dataset.run_id) run_description = loaded_dataset._get_run_description_from_db() print(run_description) + + +def test_initialize_empty_dataset(create_dummy_database): + from qcodes import Measurement + + with create_dummy_database(): + msmt = Measurement() + # msmt.register_parameter(p1_set) + # msmt.register_parameter(p1_get, setpoints=(p1_set,)) + with msmt.run(allow_empty_dataset=True) as datasaver: + pass \ No newline at end of file From dbe5fcd8aca4f968210d1dc1d333fb639dae0322 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Apr 2022 19:13:35 +0000 Subject: [PATCH 018/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 11 +++++------ .../measurement_loop/test_measurement_loop_basics.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 82032d635fd..710677a33ac 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -12,7 +12,11 @@ from qcodes import config as qcodes_config from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement +from qcodes.dataset.descriptions.rundescriber import RunDescriber +from qcodes.dataset.descriptions.versioning import serialization as serial +from qcodes.dataset.descriptions.versioning.converters import new_to_old from qcodes.dataset.measurements import Measurement +from qcodes.dataset.sqlite.queries import add_parameter, update_run_description from qcodes.instrument.base import InstrumentBase from qcodes.instrument.parameter import DelegateParameter, MultiParameter, Parameter from qcodes.instrument.sweep_values import SweepValues @@ -24,10 +28,6 @@ get_last_input_cells, using_ipython, ) -from qcodes.dataset.descriptions.rundescriber import RunDescriber -from qcodes.dataset.descriptions.versioning import serialization as serial -from qcodes.dataset.descriptions.versioning.converters import new_to_old -from qcodes.dataset.sqlite.queries import add_parameter, update_run_description RAW_VALUE_TYPES = (float, int, bool, np.ndarray, np.integer, np.floating, np.bool_, type(None)) @@ -273,8 +273,7 @@ def _update_interdependencies(self): # Generate new paramspecs with matching RunDescriber dataset._rundescriber = RunDescriber( - self.measurement._interdeps, - shapes=self.measurement._shapes + self.measurement._interdeps, shapes=self.measurement._shapes ) paramspecs = new_to_old(dataset._rundescriber.interdeps).paramspecs diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 8153d395a82..726f60dc85a 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -247,4 +247,4 @@ def test_initialize_empty_dataset(create_dummy_database): # msmt.register_parameter(p1_set) # msmt.register_parameter(p1_get, setpoints=(p1_set,)) with msmt.run(allow_empty_dataset=True) as datasaver: - pass \ No newline at end of file + pass From 64468a6f2f1713befd1f559438c808e7e79b8817 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 6 Apr 2022 20:16:21 +0200 Subject: [PATCH 019/122] fix: Update data cache --- qcodes/dataset/measurement_loop.py | 13 +-- .../test_measurement_loop_basics.py | 81 +++---------------- 2 files changed, 21 insertions(+), 73 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 710677a33ac..f192ab68f5d 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -70,11 +70,7 @@ def initialize(self): self.measurement = Measurement(name=self.name) # Create measurement Runner - # TODO remove cache - self.runner = self.measurement.run( - allow_empty_dataset=True, - in_memory_cache=False - ) + self.runner = self.measurement.run(allow_empty_dataset=True) # Create measurement Dataset self.datasaver = self.runner.__enter__() @@ -271,6 +267,7 @@ def _update_interdependencies(self): # Update DataSaver self.datasaver._interdeps = self.measurement._interdeps + # Update DataSet # Generate new paramspecs with matching RunDescriber dataset._rundescriber = RunDescriber( self.measurement._interdeps, shapes=self.measurement._shapes @@ -291,6 +288,12 @@ def _update_interdependencies(self): update_run_description(dataset.conn, dataset.run_id, desc_str) + # Update dataset cache + cache_data = self.dataset._cache._data + interdeps_empty_dict = dataset._rundescriber.interdeps._empty_data_dict() + for key, val in interdeps_empty_dict.items(): + cache_data.setdefault(key, val) + class MeasurementLoop: """Class to perform measurements diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 726f60dc85a..bf662d0ac9c 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -12,8 +12,6 @@ from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep from qcodes.utils.dataset.doNd import LinSweep -# def get_data_array(dataset, label): - @pytest.fixture def create_dummy_database(): @@ -32,6 +30,19 @@ def func_context_manager(): return func_context_manager +def test_original_dond(create_dummy_database): + with create_dummy_database(): + from qcodes.utils.dataset.doNd import dond, LinSweep + + p1_get = ManualParameter("p1_get", initial_value=1) + p2_get = ManualParameter("p2_get", initial_value=1) + p1_set = ManualParameter("p1_set") + dond( + LinSweep(p1_set, 0, 1, 101), + p1_get, p2_get + ) + + def test_create_measurement(create_dummy_database): with create_dummy_database(): MeasurementLoop("test") @@ -173,72 +184,6 @@ def test_2D_measurement_initialization(create_dummy_database): msmt.measure(p1_get) -def test_dataset_registering_shared_set(create_dummy_database): - from qcodes import Measurement - - with create_dummy_database(): - p1_get = ManualParameter("p1_get") - p1_set = ManualParameter("p1_set") - - p2_get = ManualParameter("p2_get") - - msmt = Measurement() - msmt.register_parameter(p1_set) - msmt.register_parameter(p1_get, setpoints=(p1_set,)) - # TODO allow cache - with msmt.run(in_memory_cache=False) as datasaver: - dataset = datasaver.dataset - - for k, set_v in enumerate(np.linspace(0, 25, 10)): - p1_set(set_v) - datasaver.add_result((p1_set, set_v), (p1_get, 123)) - if not k: - msmt.register_parameter(p2_get, setpoints=(p1_set,)) - # update_interdependencies(msmt, datasaver) - - datasaver.add_result((p1_set, set_v), (p2_get, 124)) - - loaded_dataset = load_by_id(dataset.run_id) - run_description = loaded_dataset._get_run_description_from_db() - print(run_description) - - -def test_dataset_registering_separate_set(create_dummy_database): - from qcodes import Measurement - - with create_dummy_database(): - p1_get = ManualParameter("p1_get") - p1_set = ManualParameter("p1_set") - - p2_set = ManualParameter("p2_set") - p2_get = ManualParameter("p2_get") - - msmt = Measurement() - msmt.register_parameter(p1_set) - msmt.register_parameter(p1_get, setpoints=(p1_set,)) - # TODO allow cache - with msmt.run(in_memory_cache=False) as datasaver: - dataset = datasaver.dataset - - for set_v in np.linspace(0, 25, 10): - p1_set(set_v) - datasaver.add_result((p1_set, set_v), (p1_get, 123)) - - # Add new parameters - msmt.register_parameter(p2_set) - msmt.register_parameter(p2_get, setpoints=(p2_set,)) - # update_interdependencies(msmt, datasaver) - - print(msmt._interdeps) - for set_v in np.linspace(0, 25, 10): - p2_set(set_v) - datasaver.add_result((p2_set, set_v), (p2_get, 124)) - - loaded_dataset = load_by_id(dataset.run_id) - run_description = loaded_dataset._get_run_description_from_db() - print(run_description) - - def test_initialize_empty_dataset(create_dummy_database): from qcodes import Measurement From 39ccc0b22e11ab742fc32d3256f93f3c12cc1675 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 6 Apr 2022 20:40:11 +0200 Subject: [PATCH 020/122] test nested measurement and no_parameter --- .../test_measurement_loop_basics.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index bf662d0ac9c..336b13db5af 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -193,3 +193,60 @@ def test_initialize_empty_dataset(create_dummy_database): # msmt.register_parameter(p1_get, setpoints=(p1_set,)) with msmt.run(allow_empty_dataset=True) as datasaver: pass + + +def test_nested_measurement(create_dummy_database): + def nested_measurement(): + # Initialize parameters + p1_get = ManualParameter("p1_get") + p1_set = ManualParameter("p1_set") + + with MeasurementLoop("test") as msmt: + for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + assert p1_set() == val + p1_get(val + 1) + msmt.measure(p1_get) + + + with create_dummy_database(): + # Initialize parameters + p2_set = ManualParameter("p2_set") + + with MeasurementLoop("test") as msmt: + for val2 in Sweep(LinSweep(p2_set, 0, 1, 11)): + assert p2_set() == val2 + nested_measurement() + + data = msmt.dataset + assert data.name == "test" + assert data.parameters == "p2_set,p1_set,p1_get" + + arrays = data.get_parameter_data() + data_array = arrays["p1_get"]["p1_get"] + + assert np.allclose(data_array, np.tile(np.linspace(1, 2, 11), (11, 1))) + + assert np.allclose( + arrays["p1_get"]["p2_set"], np.tile(np.linspace(0, 1, 11), (11, 1)).transpose() + ) + + assert np.allclose( + arrays["p1_get"]["p1_set"], np.tile(np.linspace(0, 1, 11), (11, 1)) + ) + + +def test_measurement_no_parameter(create_dummy_database): + with create_dummy_database(): + with MeasurementLoop("test") as msmt: + for val in Sweep(np.linspace(0, 1, 11), 'p1_set', label='p1 label', unit='V'): + msmt.measure(val+1, name='p1_get') + + data = msmt.dataset + assert data.name == "test" + assert data.parameters == "p1_set,p1_get" + + arrays = data.get_parameter_data() + data_arrays = arrays["p1_get"] + + assert np.allclose(data_arrays["p1_get"], np.linspace(1, 2, 11)) + assert np.allclose(data_arrays["p1_set"], np.linspace(0, 1, 11)) \ No newline at end of file From afc0a94bb04004eaca1211c6a344cac4ef532569 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 18:40:35 +0000 Subject: [PATCH 021/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../dataset/measurement_loop/test_measurement_loop_basics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 336b13db5af..619f22f5817 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -32,7 +32,7 @@ def func_context_manager(): def test_original_dond(create_dummy_database): with create_dummy_database(): - from qcodes.utils.dataset.doNd import dond, LinSweep + from qcodes.utils.dataset.doNd import LinSweep, dond p1_get = ManualParameter("p1_get", initial_value=1) p2_get = ManualParameter("p2_get", initial_value=1) @@ -249,4 +249,4 @@ def test_measurement_no_parameter(create_dummy_database): data_arrays = arrays["p1_get"] assert np.allclose(data_arrays["p1_get"], np.linspace(1, 2, 11)) - assert np.allclose(data_arrays["p1_set"], np.linspace(0, 1, 11)) \ No newline at end of file + assert np.allclose(data_arrays["p1_set"], np.linspace(0, 1, 11)) From c3ffca398344fa95321e0ef7e9f8930e5f861633 Mon Sep 17 00:00:00 2001 From: The Beefy One v2 Date: Tue, 3 May 2022 07:06:11 +0200 Subject: [PATCH 022/122] add Measurement.enteractions --- qcodes/dataset/measurements.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/measurements.py b/qcodes/dataset/measurements.py index 3894bdebf2f..a4ecf2d6f54 100644 --- a/qcodes/dataset/measurements.py +++ b/qcodes/dataset/measurements.py @@ -694,6 +694,8 @@ class Measurement: produced by the measurement. If not given, a default value of 'results' is used for the dataset. """ + enteractions = [] + exitactions = [] def __init__( self, @@ -1231,8 +1233,8 @@ def run( if write_in_background is None: write_in_background = qc.config.dataset.write_in_background return Runner( - self.enteractions, - self.exitactions, + [*self.enteractions, *Measurement.enteractions], + [*self.exitactions, *Measurement.exitactions], self.experiment, station=self.station, write_period=self._write_period, From 8b491458a70aa2061bc61c019b4c429b0a99aa0f Mon Sep 17 00:00:00 2001 From: Serwan Date: Mon, 20 Jun 2022 16:54:48 +0200 Subject: [PATCH 023/122] Add repetitionSweep --- qcodes/dataset/measurement_loop.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index f192ab68f5d..49ece834900 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1369,3 +1369,15 @@ def exit_sweep(self): msmt = running_measurement() msmt.step_out(reduce_dimension=True) raise StopIteration + + +class RepetitionSweep(Sweep): + def __init__(self, repetitions, start=0, name='repetition', label='Repetition', unit=None, reverse=False, restore=False): + self.start = start + self.repetitions = repetitions + + super().__init__(self.sequence, name, label, unit, reverse, restore) + + @property + def sequence(self): + return self.start + np.arange(self.repetitions) \ No newline at end of file From e1ea91c77de6dde6a4a52b2337648b11870c30fd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Jun 2022 14:55:13 +0000 Subject: [PATCH 024/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 49ece834900..97c966922b7 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1380,4 +1380,4 @@ def __init__(self, repetitions, start=0, name='repetition', label='Repetition', @property def sequence(self): - return self.start + np.arange(self.repetitions) \ No newline at end of file + return self.start + np.arange(self.repetitions) From b4f57e11d9194aaaea2668cd8a14c1cf9b26dce5 Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Mon, 20 Jun 2022 18:40:05 +0200 Subject: [PATCH 025/122] minor bugfixes --- qcodes/dataset/measurement_loop.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 97c966922b7..41eeba7b798 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -778,8 +778,8 @@ def _measure_value(self, value, name, parameter=None, label=None, unit=None): value = int(value) elif isinstance(value, np.floating): value = float(value) - elif isinstance(value, np.bool_): - value = bool(value) + elif isinstance(value, (bool, np.bool_)): + value = int(value) result = value self.data_handler.add_measurement_result( @@ -1375,9 +1375,6 @@ class RepetitionSweep(Sweep): def __init__(self, repetitions, start=0, name='repetition', label='Repetition', unit=None, reverse=False, restore=False): self.start = start self.repetitions = repetitions + sequence = self.start + np.arange(self.repetitions) - super().__init__(self.sequence, name, label, unit, reverse, restore) - - @property - def sequence(self): - return self.start + np.arange(self.repetitions) + super().__init__(sequence, name, label, unit, reverse, restore) \ No newline at end of file From dfb70855a57838898423adc646b750231303ea67 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Jun 2022 16:40:22 +0000 Subject: [PATCH 026/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 41eeba7b798..de9cd25b497 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1377,4 +1377,4 @@ def __init__(self, repetitions, start=0, name='repetition', label='Repetition', self.repetitions = repetitions sequence = self.start + np.arange(self.repetitions) - super().__init__(sequence, name, label, unit, reverse, restore) \ No newline at end of file + super().__init__(sequence, name, label, unit, reverse, restore) From 9326822b1f4cf89d5ddffdb2d7d6fe568a7e234c Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 20 Jul 2022 09:32:15 +0200 Subject: [PATCH 027/122] upgrading Sweep --- qcodes/dataset/measurement_loop.py | 118 ++++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 21 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index de9cd25b497..67329848c57 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -18,7 +18,7 @@ from qcodes.dataset.measurements import Measurement from qcodes.dataset.sqlite.queries import add_parameter, update_run_description from qcodes.instrument.base import InstrumentBase -from qcodes.instrument.parameter import DelegateParameter, MultiParameter, Parameter +from qcodes.instrument.parameter import _BaseParameter, DelegateParameter, MultiParameter, Parameter from qcodes.instrument.sweep_values import SweepValues from qcodes.station import Station from qcodes.utils.dataset.doNd import AbstractSweep @@ -1210,7 +1210,7 @@ def __next__(self): -class Sweep: +class BaseSweep: """Sweep over an iterable inside a Measurement Args: @@ -1219,10 +1219,9 @@ class Sweep: If the sequence name: Name of sweep. Not needed if a Parameter is passed unit: unit of sweep. Not needed if a Parameter is passed - reverse: Sweep over sequence in opposite order. - The data is also stored in reverse. restore: Stores the state of a parameter before sweeping it, then restores the original value upon exiting the loop. + delay: Wait time after setting value (default zero). Examples: ``` @@ -1234,7 +1233,7 @@ class Sweep: for param_val in Sweep(p. ``` """ - def __init__(self, sequence, name=None, label=None, unit=None, reverse=False, restore=False): + def __init__(self, sequence, name=None, label=None, unit=None, parameter=None, restore=False, delay=None): msmt = running_measurement() if msmt is None: raise RuntimeError("Cannot create a sweep outside a Measurement") @@ -1248,13 +1247,14 @@ def __init__(self, sequence, name=None, label=None, unit=None, reverse=False, re self.name = name self.label = label self.unit = unit + self.parameter = parameter self.sequence = sequence self.dimension = len(running_measurement().loop_shape) self.loop_index = None self.iterator = None - self.reverse = reverse self.restore = restore + self.delay = delay # Check if this is the first sweep # Useful to know when to initialize dataset @@ -1276,12 +1276,8 @@ def __iter__(self): else: raise NotImplementedError("Unable to restore non-parameter values.") - if self.reverse: - self.loop_index = len(self.sequence) - 1 - self.iterator = iter(self.sequence[::-1]) - else: - self.loop_index = 0 - self.iterator = iter(self.sequence) + self.loop_index = 0 + self.iterator = iter(self.sequence) running_measurement().loop_shape += (len(self.sequence),) running_measurement().loop_indices += (self.loop_index,) @@ -1324,12 +1320,18 @@ def __next__(self): pass self.exit_sweep() - if isinstance(self.sequence, SweepValues): - self.sequence.set(sweep_value) + # Set parameter if passed along + if self.parameter is not None: + self.parameter(sweep_value) + + # Optional wait after settings value + if self.delay: + sleep(self.delay) + self.setpoint_info['latest_value'] = sweep_value - self.loop_index += 1 if not self.reverse else -1 + self.loop_index += 1 return sweep_value @@ -1337,21 +1339,22 @@ def initialize(self): msmt = running_measurement() if msmt.action_indices in msmt.setpoint_list: return msmt.setpoint_list[msmt.action_indices] - else: - # Determine sweep parameter + + # Determine sweep parameter + if self.parameter is None: if isinstance(self.sequence, _IterateDondSweep): # sweep is a doNd sweep that already has a parameter - set_parameter = self.sequence.parameter + self.parameter = self.sequence.parameter else: # Need to create a parameter - set_parameter = Parameter( + self.parameter = Parameter( name=self.name, label=self.label, unit=self.unit ) setpoint_info = { - 'parameter': set_parameter, + 'parameter': self.parameter, 'latest_value': None, 'registered': False } @@ -1371,7 +1374,80 @@ def exit_sweep(self): raise StopIteration -class RepetitionSweep(Sweep): +class Sweep(BaseSweep): + sequence_keywords = ['begin', 'to', 'around', 'num', 'step'] + base_keywords = ['delay', 'name', 'label', 'unit', 'restore'] + + def __init__(self, *args, begin=None, to=None, around=None, num=None, step=None, delay=None, name=None, label=None, unit=None, restore=None): + kwargs = {**self.sequence_keywords, **self.base_keywords} + + sequence_kwargs, base_kwargs = self.transform_args_to_kwargs(*args, **kwargs) + + sequence = self.generate_sequence(**sequence_kwargs) + + super().__init__(sequence=sequence, **base_kwargs) + + def transform_args_to_kwargs(self, *args, **kwargs): + kwargs = kwargs.copy() # Make a copy of kwargs so original does not change + + if len(args) == 1: # Sweep([1,2,3], 'name') + assert isinstance(args[0], Iterable) + assert 'name' in kwargs + kwargs['sequence'], = args + elif len(args) == 2: + if isinstance(args[0], _BaseParameter): # Sweep(parameter, [1,2,3]) + assert isinstance(args[1], Iterable) + kwargs['parameter'], kwargs['sequence'] = args + elif isinstance(args[0], Iterable): # Sweep([1,2,3], 'name') + assert isinstance(args[1], str) + assert kwargs.get('name') is None + kwargs['sequence'], kwargs['name'] = args + else: + raise SyntaxError( + 'Unknown sweep syntax. Either use "Sweep(parameter, sequence)" or ' + 'Sweep(sequence, name)"' + ) + elif len(args) == 3: # Sweep(parameter, 0, 1) + assert isinstance(args[0], _BaseParameter) + assert isinstance(args[1], (float, int)) + assert isinstance(args[2], (float, int)) + assert kwargs['begin'] is None + assert kwargs['to'] is None + kwargs['parameter'], kwargs['begin'], kwargs['to'] = args + + if not kwargs['step'] and not kwargs['num']: + if not hasattr(parameter, '_default_sweep_points'): + raise SyntaxError( + 'Cannot determine many measurement points to use. ' + 'Either provide "step", "num", or set parameter._default_sweep_points' + ) + else: + kwargs['num'] = parameter._default_sweep_points + elif len(args) == 4: # Sweep(parameter, 0, 1, 151) + assert isinstance(args[0], _BaseParameter) + assert isinstance(args[1], (float, int)) + assert isinstance(args[2], (float, int)) + assert isinstance(args[3], (float, int)) + assert kwargs['begin'] is None + assert kwargs['to'] is None + assert kwargs['num'] is None + kwargs['parameter'], kwargs['begin'], kwargs['to'], kwargs['num'] = args + + if kwargs['parameter'] is not None: + kwargs.setdefault('name', parameter.name) + kwargs.setdefault('label', parameter.label) + kwargs.setdefault('unit', parameter.unit) + + sequence_kwargs = {key: kwargs[key] for key in self.sequence_keywords} + base_kwargs = {key: kwargs[key] for key in self.base_keywords} + return sequence_kwargs, base_kwargs + + def generate_sequence(self, **kwargs): + pass + + + +class RepetitionSweep(BaseSweep): def __init__(self, repetitions, start=0, name='repetition', label='Repetition', unit=None, reverse=False, restore=False): self.start = start self.repetitions = repetitions From 431d753c12e3a1b3b197a5f8b1e0d3aa66d33d04 Mon Sep 17 00:00:00 2001 From: Serwan Date: Fri, 29 Jul 2022 15:24:22 +0200 Subject: [PATCH 028/122] upgrading sweep --- qcodes/dataset/measurement_loop.py | 149 +++++++++++++++++++++-------- 1 file changed, 109 insertions(+), 40 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 67329848c57..1f7eb8faeab 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1219,8 +1219,8 @@ class BaseSweep: If the sequence name: Name of sweep. Not needed if a Parameter is passed unit: unit of sweep. Not needed if a Parameter is passed - restore: Stores the state of a parameter before sweeping it, - then restores the original value upon exiting the loop. + revert: Stores the state of a parameter before sweeping it, + then reverts the original value upon exiting the loop. delay: Wait time after setting value (default zero). Examples: @@ -1233,7 +1233,7 @@ class BaseSweep: for param_val in Sweep(p. ``` """ - def __init__(self, sequence, name=None, label=None, unit=None, parameter=None, restore=False, delay=None): + def __init__(self, sequence, name=None, label=None, unit=None, parameter=None, revert=False, delay=None): msmt = running_measurement() if msmt is None: raise RuntimeError("Cannot create a sweep outside a Measurement") @@ -1253,7 +1253,7 @@ def __init__(self, sequence, name=None, label=None, unit=None, parameter=None, r self.dimension = len(running_measurement().loop_shape) self.loop_index = None self.iterator = None - self.restore = restore + self.revert = revert self.delay = delay # Check if this is the first sweep @@ -1270,11 +1270,11 @@ def __iter__(self): "Cannot create a Sweep while another measurement " "is already running in a different thread." ) - if self.restore: + if self.revert: if isinstance(self.sequence, SweepValues): running_measurement().mask(self.sequence.parameter, self.sequence.parameter.get()) else: - raise NotImplementedError("Unable to restore non-parameter values.") + raise NotImplementedError("Unable to revert non-parameter values.") self.loop_index = 0 self.iterator = iter(self.sequence) @@ -1312,7 +1312,7 @@ def __next__(self): action_indices[-1] = 0 msmt.action_indices = tuple(action_indices) except StopIteration: # Reached end of iteration - if self.restore: + if self.revert: if isinstance(self.sequence, SweepValues): msmt.unmask(self.sequence.parameter) else: @@ -1375,11 +1375,22 @@ def exit_sweep(self): class Sweep(BaseSweep): - sequence_keywords = ['begin', 'to', 'around', 'num', 'step'] - base_keywords = ['delay', 'name', 'label', 'unit', 'restore'] + sequence_keywords = ['start', 'stop', 'around', 'num', 'step'] + base_keywords = ['delay', 'name', 'label', 'unit', 'revert'] - def __init__(self, *args, begin=None, to=None, around=None, num=None, step=None, delay=None, name=None, label=None, unit=None, restore=None): - kwargs = {**self.sequence_keywords, **self.base_keywords} + def __init__(self, *args, start=None, stop=None, around=None, num=None, step=None, delay=None, name=None, label=None, unit=None, revert=None): + kwargs = dict( + start=start, + stop=stop, + around=around, + num=num, + step=step, + delay=delay, + name=name, + label=label, + unit=unit, + revert=revert + ) sequence_kwargs, base_kwargs = self.transform_args_to_kwargs(*args, **kwargs) @@ -1388,16 +1399,52 @@ def __init__(self, *args, begin=None, to=None, around=None, num=None, step=None, super().__init__(sequence=sequence, **base_kwargs) def transform_args_to_kwargs(self, *args, **kwargs): + """Transforms sweep initialization args to kwargs. + Allowed args are: + + 1 arg: + - Sweep([1,2,3], name='name') + : sweep over sequence [1,2,3] with sweep array name 'name' + Note that kwarg "name" must be provided + - Sweep(parameter, stop=stop_val) + : sweep "parameter" from current value to "stop_val" + - Sweep(parameter, around=around_val) + : sweep "parameter" around current value with range "around_val" + 2 args: + - Sweep(parameter, [1,2,3]) + : sweep "parameter" over sequence [1,2,3] + - Sweep(parameter, stop_val) + : sweep "parameter" from current value to "stop_val" + - Sweep([1,2,3], 'name') + : sweep over sequence [1,2,3] with sweep array name 'name' + 3 args: + - Sweep(parameter, start_val, stop_val) + : sweep "parameter" from "start_val" to "stop_val" + If "num" or "step" is not given as kwarg, it will check if "num" or "step" + if set in dict "parameter.sweep_defaults" and use that, or raise an error otherwise. + 4 args: + - Sweep(parameter, start_val, stop_val, num) + : Sweep "parameter" from "start_val" to "stop_val" with "num" number of points + """ kwargs = kwargs.copy() # Make a copy of kwargs so original does not change - if len(args) == 1: # Sweep([1,2,3], 'name') - assert isinstance(args[0], Iterable) - assert 'name' in kwargs - kwargs['sequence'], = args + if len(args) == 1: # Sweep([1,2,3], name='name') + if isinstance(args[0], Iterable): + assert kwargs.get('name') is not None, "Must provide name if sweeping iterable" + kwargs['sequence'], = args + elif isinstance(args[0], _BaseParameter): + assert kwargs.get('stop') is not None or kwargs.get('around') is not None, \ + "Must provide stop value for parameter" + else: + raise SyntaxError('Sweep with 1 arg must have iterable or parameter as arg') elif len(args) == 2: if isinstance(args[0], _BaseParameter): # Sweep(parameter, [1,2,3]) - assert isinstance(args[1], Iterable) - kwargs['parameter'], kwargs['sequence'] = args + if isinstance(args[1], Iterable): + kwargs['parameter'], kwargs['sequence'] = args + elif isinstance(args[1], (int, float)): + kwargs['parameter'], kwargs['stop'] = args + else: + raise SyntaxError('Sweep ') elif isinstance(args[0], Iterable): # Sweep([1,2,3], 'name') assert isinstance(args[1], str) assert kwargs.get('name') is None @@ -1411,46 +1458,68 @@ def transform_args_to_kwargs(self, *args, **kwargs): assert isinstance(args[0], _BaseParameter) assert isinstance(args[1], (float, int)) assert isinstance(args[2], (float, int)) - assert kwargs['begin'] is None - assert kwargs['to'] is None - kwargs['parameter'], kwargs['begin'], kwargs['to'] = args - - if not kwargs['step'] and not kwargs['num']: - if not hasattr(parameter, '_default_sweep_points'): + assert kwargs.get('start') is None + assert kwargs.get('stop') is None + kwargs['parameter'], kwargs['start'], kwargs['stop'] = args + + if not kwargs.get('step') and not kwargs.get('num'): + parameter_sweep_defaults = getattr(kwargs['parameter'], 'sweep_defaults', {}) + if 'num' in parameter_sweep_defaults: + kwargs['num'] = parameter_sweep_defaults['num'] + elif 'step' in parameter_sweep_defaults: + kwargs['step'] = parameter_sweep_defaults['step'] + else: raise SyntaxError( 'Cannot determine many measurement points to use. ' 'Either provide "step", "num", or set parameter._default_sweep_points' ) - else: - kwargs['num'] = parameter._default_sweep_points elif len(args) == 4: # Sweep(parameter, 0, 1, 151) assert isinstance(args[0], _BaseParameter) assert isinstance(args[1], (float, int)) assert isinstance(args[2], (float, int)) assert isinstance(args[3], (float, int)) - assert kwargs['begin'] is None - assert kwargs['to'] is None - assert kwargs['num'] is None - kwargs['parameter'], kwargs['begin'], kwargs['to'], kwargs['num'] = args - - if kwargs['parameter'] is not None: - kwargs.setdefault('name', parameter.name) - kwargs.setdefault('label', parameter.label) - kwargs.setdefault('unit', parameter.unit) - - sequence_kwargs = {key: kwargs[key] for key in self.sequence_keywords} - base_kwargs = {key: kwargs[key] for key in self.base_keywords} + assert kwargs.get('start') is None + assert kwargs.get('stop') is None + assert kwargs.get('num') is None + kwargs['parameter'], kwargs['start'], kwargs['stop'], kwargs['num'] = args + + # Use parameter name, label, and unit if not explicitly provided + if kwargs.get('parameter') is not None: + kwargs.setdefault('name', kwargs['parameter'].name) + kwargs.setdefault('label', kwargs['parameter'].label) + kwargs.setdefault('unit', kwargs['parameter'].unit) + + sequence_kwargs = {key: kwargs.get(key) for key in self.sequence_keywords} + base_kwargs = {key: kwargs.get(key) for key in self.base_keywords} return sequence_kwargs, base_kwargs def generate_sequence(self, **kwargs): - pass + if kwargs['around'] is not None and (kwargs['start'] is not None or kwargs['stop'] is not None): + raise SyntaxError('Cannot pass kwarg "around" and also "start" or "stop') + + # Convert "around" to "start" and "stop" using parameter current value + if kwargs['around'] is not None: + assert kwargs['parameter'] is not None, 'Cannot use kwarg "around" without a parameter' + center_value = kwargs['parameter']() + kwargs['start'] = center_value - kwargs['around'] + kwargs['stop'] = center_value + kwargs['around'] + + # Transform "step" into "num" + if kwargs['step'] is not None and kwargs['num'] is None: + num_float = abs((kwargs['stop'] - kwargs['start']) / kwargs['step']) + kwargs['num'] = int(np.ceil(num_float)) + 1 + + sequence = np.linspace(kwargs['start'], kwargs['stop'], kwargs['num']) + + return sequence + class RepetitionSweep(BaseSweep): - def __init__(self, repetitions, start=0, name='repetition', label='Repetition', unit=None, reverse=False, restore=False): + def __init__(self, repetitions, start=0, name='repetition', label='Repetition', unit=None, reverse=False, revert=False): self.start = start self.repetitions = repetitions sequence = self.start + np.arange(self.repetitions) - super().__init__(sequence, name, label, unit, reverse, restore) + super().__init__(sequence, name, label, unit, reverse, revert) From 2ab456663d05dde7c05010a3a1f3d2c7f1f2b5c3 Mon Sep 17 00:00:00 2001 From: Serwan Date: Mon, 1 Aug 2022 20:37:33 +0200 Subject: [PATCH 029/122] Nearly finished with Sweep --- qcodes/dataset/measurement_loop.py | 209 ++++++++++++++++++++--------- 1 file changed, 146 insertions(+), 63 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 1f7eb8faeab..a57d471931e 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1,10 +1,7 @@ -import contextlib import logging import threading import traceback -from collections import Counter from datetime import datetime -from enum import unique from time import perf_counter, sleep from typing import Any, Callable, Dict, Iterable, List, Sequence, Tuple, Union @@ -1233,15 +1230,11 @@ class BaseSweep: for param_val in Sweep(p. ``` """ - def __init__(self, sequence, name=None, label=None, unit=None, parameter=None, revert=False, delay=None): - msmt = running_measurement() - if msmt is None: - raise RuntimeError("Cannot create a sweep outside a Measurement") - + def __init__(self, sequence, name=None, label=None, unit=None, parameter=None, revert=False, delay=None, initial_delay=None): if isinstance(sequence, AbstractSweep): sequence = _IterateDondSweep(sequence) elif not isinstance(sequence, Iterable): - raise SyntaxError("Sweep sequence must be iterable") + raise SyntaxError(f"Sweep sequence must be iterable, not {type(sequence)}") # Properties for the data array self.name = name @@ -1250,19 +1243,20 @@ def __init__(self, sequence, name=None, label=None, unit=None, parameter=None, r self.parameter = parameter self.sequence = sequence - self.dimension = len(running_measurement().loop_shape) + self.dimension = None self.loop_index = None self.iterator = None self.revert = revert self.delay = delay + self.initial_delay = initial_delay - # Check if this is the first sweep - # Useful to know when to initialize dataset - msmt = running_measurement() + # setpoint_info will be populated once the sweep starts + self.setpoint_info = None - # Create setpoint_list - self.initialize() - self.setpoint_info = msmt.setpoint_list[msmt.action_indices] + # Validate values + if self.parameter is not None and hasattr(self.parameter, 'validate'): + for value in self.sequence: + self.parameter.validate(value) def __iter__(self): if threading.current_thread() is not MeasurementLoop.measurement_thread: @@ -1270,18 +1264,27 @@ def __iter__(self): "Cannot create a Sweep while another measurement " "is already running in a different thread." ) + + msmt = running_measurement() + if msmt is None: + raise RuntimeError("Cannot start a sweep outside a Measurement") + if self.revert: if isinstance(self.sequence, SweepValues): - running_measurement().mask(self.sequence.parameter, self.sequence.parameter.get()) + msmt.mask(self.sequence.parameter, self.sequence.parameter.get()) else: raise NotImplementedError("Unable to revert non-parameter values.") self.loop_index = 0 + self.dimension = len(msmt.loop_shape) self.iterator = iter(self.sequence) - running_measurement().loop_shape += (len(self.sequence),) - running_measurement().loop_indices += (self.loop_index,) - running_measurement().action_indices += (0,) + # Create setpoint_list + self.setpoint_info = self.initialize() + + msmt.loop_shape += (len(self.sequence),) + msmt.loop_indices += (self.loop_index,) + msmt.action_indices += (0,) return self @@ -1325,10 +1328,11 @@ def __next__(self): self.parameter(sweep_value) # Optional wait after settings value + if self.initial_delay and self.loop_index == 0: + sleep(self.initial_delay) if self.delay: sleep(self.delay) - self.setpoint_info['latest_value'] = sweep_value self.loop_index += 1 @@ -1373,12 +1377,49 @@ def exit_sweep(self): msmt.step_out(reduce_dimension=True) raise StopIteration + def execute( + self, + name: str = None, + measure_params: Iterable = None, + repetitions: int = 1, + sweep: Union[Iterable, AbstractSweep] = None + ): + # Get "measure_params" from station if not provided + if measure_params is None: + station = Station.default + if station is None or not getattr(station, 'measure_params', None): + raise RuntimeError( + 'Cannot determine parameters to measure. ' + 'Either provide measure_params, or set station.measure_params' + ) + measure_params = station.measure_params + + # Ensure sweeps is a list + if isinstance(sweep, BaseSweep): + sweeps = [sweep] + elif isinstance(sweep, Iterable): + sweeps = list(sweep) + + # Add repetition as a sweep if > 1 + if repetitions > 1: + repetition_sweep = BaseSweep(range(repetitions), name='repetition') + sweeps = [repetition_sweep] + sweeps + + # Determine "name" if not provided from sweeps + if name is None: + dimensionality = 1 + len(sweep) + sweep_names = [sweep.name for sweep in sweeps] + [str(self.name)] + name = f'{dimensionality}D_sweep_' + '_'.join(sweep_names) + + with MeasurementLoop(name) as msmt: + measure_sweeps(sweeps=sweeps, measure_params=measure_params, msmt=msmt) + class Sweep(BaseSweep): - sequence_keywords = ['start', 'stop', 'around', 'num', 'step'] - base_keywords = ['delay', 'name', 'label', 'unit', 'revert'] + sequence_keywords = ['start', 'stop', 'around', 'num', 'step', 'parameter', 'sequence'] + base_keywords = ['delay', 'initial_delay', 'name', 'label', 'unit', 'revert', 'parameter'] - def __init__(self, *args, start=None, stop=None, around=None, num=None, step=None, delay=None, name=None, label=None, unit=None, revert=None): + def __init__(self, *args, start=None, stop=None, around=None, num=None, step=None, delay=None, initial_delay=None, name=None, label=None, unit=None, revert=None): kwargs = dict( start=start, stop=stop, @@ -1386,19 +1427,21 @@ def __init__(self, *args, start=None, stop=None, around=None, num=None, step=Non num=num, step=step, delay=delay, + initial_delay=initial_delay, name=name, label=label, unit=unit, revert=revert ) - sequence_kwargs, base_kwargs = self.transform_args_to_kwargs(*args, **kwargs) + sequence_kwargs, base_kwargs = self._transform_args_to_kwargs(*args, **kwargs) - sequence = self.generate_sequence(**sequence_kwargs) + self._explicit_sequence = None + self.sequence = self._generate_sequence(**sequence_kwargs) - super().__init__(sequence=sequence, **base_kwargs) + super().__init__(sequence=self.sequence, **base_kwargs) - def transform_args_to_kwargs(self, *args, **kwargs): + def _transform_args_to_kwargs(self, *args, **kwargs): """Transforms sweep initialization args to kwargs. Allowed args are: @@ -1426,8 +1469,6 @@ def transform_args_to_kwargs(self, *args, **kwargs): - Sweep(parameter, start_val, stop_val, num) : Sweep "parameter" from "start_val" to "stop_val" with "num" number of points """ - kwargs = kwargs.copy() # Make a copy of kwargs so original does not change - if len(args) == 1: # Sweep([1,2,3], name='name') if isinstance(args[0], Iterable): assert kwargs.get('name') is not None, "Must provide name if sweeping iterable" @@ -1435,6 +1476,7 @@ def transform_args_to_kwargs(self, *args, **kwargs): elif isinstance(args[0], _BaseParameter): assert kwargs.get('stop') is not None or kwargs.get('around') is not None, \ "Must provide stop value for parameter" + kwargs['parameter'], = args else: raise SyntaxError('Sweep with 1 arg must have iterable or parameter as arg') elif len(args) == 2: @@ -1444,7 +1486,7 @@ def transform_args_to_kwargs(self, *args, **kwargs): elif isinstance(args[1], (int, float)): kwargs['parameter'], kwargs['stop'] = args else: - raise SyntaxError('Sweep ') + raise SyntaxError('Sweep with Parameter arg and second arg should h') elif isinstance(args[0], Iterable): # Sweep([1,2,3], 'name') assert isinstance(args[1], str) assert kwargs.get('name') is None @@ -1461,18 +1503,6 @@ def transform_args_to_kwargs(self, *args, **kwargs): assert kwargs.get('start') is None assert kwargs.get('stop') is None kwargs['parameter'], kwargs['start'], kwargs['stop'] = args - - if not kwargs.get('step') and not kwargs.get('num'): - parameter_sweep_defaults = getattr(kwargs['parameter'], 'sweep_defaults', {}) - if 'num' in parameter_sweep_defaults: - kwargs['num'] = parameter_sweep_defaults['num'] - elif 'step' in parameter_sweep_defaults: - kwargs['step'] = parameter_sweep_defaults['step'] - else: - raise SyntaxError( - 'Cannot determine many measurement points to use. ' - 'Either provide "step", "num", or set parameter._default_sweep_points' - ) elif len(args) == 4: # Sweep(parameter, 0, 1, 151) assert isinstance(args[0], _BaseParameter) assert isinstance(args[1], (float, int)) @@ -1489,37 +1519,90 @@ def transform_args_to_kwargs(self, *args, **kwargs): kwargs.setdefault('label', kwargs['parameter'].label) kwargs.setdefault('unit', kwargs['parameter'].unit) + # Update kwargs with sweep_defaults from parameter + if hasattr(kwargs['parameter'], 'sweep_defaults'): + for key, val in kwargs['parameter'].sweep_defaults.items(): + if kwargs.get(key) is None: + kwargs[key] = val + sequence_kwargs = {key: kwargs.get(key) for key in self.sequence_keywords} base_kwargs = {key: kwargs.get(key) for key in self.base_keywords} + print(f'{sequence_kwargs=}') # TODO removeme + print(f'{base_kwargs=}') # TODO removeme return sequence_kwargs, base_kwargs - def generate_sequence(self, **kwargs): - if kwargs['around'] is not None and (kwargs['start'] is not None or kwargs['stop'] is not None): - raise SyntaxError('Cannot pass kwarg "around" and also "start" or "stop') - - # Convert "around" to "start" and "stop" using parameter current value - if kwargs['around'] is not None: - assert kwargs['parameter'] is not None, 'Cannot use kwarg "around" without a parameter' - center_value = kwargs['parameter']() - kwargs['start'] = center_value - kwargs['around'] - kwargs['stop'] = center_value + kwargs['around'] + def _generate_sequence(self, start=None, stop=None, around=None, num=None, step=None, parameter=None, sequence=None): + """Creates a sequence from passed values""" + # Return "sequence" if explicitly provided + if sequence is not None: + return sequence + + # Verify that "around" is used with "parameter" but not with "start" and "stop" + if around is not None: + if start is not None or stop is not None: + raise SyntaxError('Cannot pass kwarg "around" and also "start" or "stop') + elif parameter is None: + raise SyntaxError('Cannot use kwarg "around" without a parameter') + + # Convert "around" to "start" and "stop" using parameter current value + center_value = parameter() + if center_value is None: + raise ValueError('Parameter must have initial value if "around" keyword is used') + start = center_value - around + stop = center_value + around + elif stop is not None: + # Use "parameter" current value if "start" is not provided + if start is None: + if parameter is None: + raise SyntaxError('Cannot use "stop" without "start" or a "parameter"') + start = parameter() + if start is None: + raise ValueError('Parameter must have initial value if start is not explicitly provided') + else: + raise SyntaxError('Must provide either "around" or "stop"') - # Transform "step" into "num" - if kwargs['step'] is not None and kwargs['num'] is None: - num_float = abs((kwargs['stop'] - kwargs['start']) / kwargs['step']) - kwargs['num'] = int(np.ceil(num_float)) + 1 + if num is not None: + sequence = np.linspace(start, stop, num) + elif step is not None: + # Ensure step is positive + step = abs(step) if stop > start else -abs(step) - sequence = np.linspace(kwargs['start'], kwargs['stop'], kwargs['num']) + sequence = np.arange(start, stop, step) + + # Append final datapoint + if abs((stop - sequence[-1]) / step) > 1e-9: + sequence = np.append(sequence, [stop]) + else: + raise SyntaxError('Cannot determine measurement points. Either provide "sequence, "step" or "num"') return sequence - - class RepetitionSweep(BaseSweep): - def __init__(self, repetitions, start=0, name='repetition', label='Repetition', unit=None, reverse=False, revert=False): + def __init__(self, repetitions, start=0, name='repetition', label='Repetition', unit=None): self.start = start self.repetitions = repetitions - sequence = self.start + np.arange(self.repetitions) + sequence = start + np.arange(repetitions) + + super().__init__(sequence, name, label, unit) + + +def measure_sweeps(sweeps: list[BaseSweep], measure_params: list[_BaseParameter], msmt: MeasurementLoop = None): + """Recursively iterate over Sweep objects, measuring measure_params in innermost loop + + Args: + sweeps: list of BaseSweep objects to sweep over + measure_params: list of parameters to measure in innermost loop + """ + if sweeps: + outer_sweep, *inner_sweeps = sweeps + + for _ in outer_sweep: + measure_sweeps(inner_sweeps, measure_params, msmt=msmt) + + else: + if msmt is None: + msmt = running_measurement() - super().__init__(sequence, name, label, unit, reverse, revert) + for measure_param in measure_params: + msmt.measure(measure_param) \ No newline at end of file From 013e3aa48421f12258f8fc24a4cb7a64a02e408b Mon Sep 17 00:00:00 2001 From: Serwan Date: Mon, 1 Aug 2022 20:37:41 +0200 Subject: [PATCH 030/122] Started adding tests for Sweep --- .../test_measurement_loop_sweep.py | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py new file mode 100644 index 00000000000..3dd61b77294 --- /dev/null +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -0,0 +1,124 @@ +import contextlib +import shutil +import tempfile +from pathlib import Path + +import numpy as np +import pytest + +from qcodes import ManualParameter, Parameter +from qcodes.dataset import initialise_or_create_database_at, load_or_create_experiment +from qcodes.dataset.data_set import load_by_id +from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep +from qcodes.utils.dataset.doNd import LinSweep + + +def test_sweep_1_arg_sequence(): + sequence = [1,2,3] + sweep = Sweep(sequence, name='sweep_name') + assert sweep.sequence == sequence + +def test_sweep_1_arg_parameter_stop(): + sweep_parameter = ManualParameter('sweep_parameter') + + # Should raise an error since it does not have an initial value + with pytest.raises(ValueError): + sweep = Sweep(sweep_parameter, stop=10, num=21) + + sweep_parameter(0) + sweep = Sweep(sweep_parameter, stop=10, num=21) + assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) + + sweep_parameter.sweep_defaults = {'num': 21} + sweep = Sweep(sweep_parameter, stop=10) + assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) + + +def test_sweep_1_arg_parameter_around(): + sweep_parameter = ManualParameter('sweep_parameter', initial_value=0) + + sweep = Sweep(sweep_parameter, around=5, num=21) + assert np.allclose(sweep.sequence, np.linspace(-5, 5, 21)) + + sweep_parameter.sweep_defaults = {'num': 21} + sweep = Sweep(sweep_parameter, around=5) + assert np.allclose(sweep.sequence, np.linspace(-5, 5, 21)) + + +def test_sweep_2_args_parameter_sequence(): + sweep_parameter = ManualParameter('sweep_parameter', initial_value=0) + + sequence = [1, 2, 3] + sweep = Sweep(sweep_parameter, sequence) + assert np.allclose(sweep.sequence, sequence) + assert sweep.parameter == sweep_parameter + + +def test_sweep_2_args_parameter_stop(): + sweep_parameter = ManualParameter('sweep_parameter') + + # No initial value + with pytest.raises(ValueError): + sweep = Sweep(sweep_parameter, 10) + with pytest.raises(ValueError): + sweep = Sweep(sweep_parameter, 10, num=21) + + sweep_parameter(0) + with pytest.raises(SyntaxError): + sweep = Sweep(sweep_parameter, 10) + + sweep = Sweep(sweep_parameter, 10, num=21) + assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) + + sweep_parameter.sweep_defaults = {'num': 21} + sweep = Sweep(sweep_parameter, 10) + assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) + + +def test_sweep_2_args_sequence_name(): + sweep_values = [1, 2, 3] + with pytest.raises(AssertionError): + sweep = Sweep(sweep_values) + + sweep = Sweep(sweep_values, 'sweep_values') + assert np.allclose(sweep.sequence, sweep_values) + + +def test_sweep_3_args_parameter_start_stop(): + sweep_parameter = ManualParameter('sweep_parameter') + + with pytest.raises(SyntaxError): + sweep = Sweep(sweep_parameter, 0, 10) + + sweep = Sweep(sweep_parameter, 0, 10, num=21) + assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) + + sweep_values = [1, 2, 3] + with pytest.raises(AssertionError): + sweep = Sweep(sweep_values) + + sweep = Sweep(sweep_values, 'sweep_values') + assert np.allclose(sweep.sequence, sweep_values) + + +def test_sweep_4_args_parameter_start_stop_num(): + sweep_parameter = ManualParameter('sweep_parameter') + + sweep = Sweep(sweep_parameter, 0, 10, 21) + assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) + + +def test_sweep_step(): + sweep = Sweep(start=0, stop=10, step=0.5) + assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) + + # Append final element since it isn't a multiple of 0.5 + sweep = Sweep(start=0, stop=9.9, step=0.5) + assert np.allclose(sweep.sequence, np.append(np.arange(0, 9.9, 0.5), [9.9])) + + +def test_error_on_iterate_sweep(): + sweep = Sweep([1,2,3], 'sweep') + + with pytest.raises(RuntimeError): + iter(sweep) \ No newline at end of file From e648ad6b0439b5aeeb2290056cd8740af043020c Mon Sep 17 00:00:00 2001 From: Serwan Date: Mon, 1 Aug 2022 21:17:07 +0200 Subject: [PATCH 031/122] fixed tests --- qcodes/dataset/measurement_loop.py | 43 ++++++++++++------- .../test_measurement_loop_basics.py | 30 +++++++------ 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index a57d471931e..68a1e149468 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1324,7 +1324,7 @@ def __next__(self): self.exit_sweep() # Set parameter if passed along - if self.parameter is not None: + if self.parameter is not None and self.parameter.settable: self.parameter(sweep_value) # Optional wait after settings value @@ -1357,20 +1357,20 @@ def initialize(self): unit=self.unit ) - setpoint_info = { - 'parameter': self.parameter, - 'latest_value': None, - 'registered': False - } + setpoint_info = { + 'parameter': self.parameter, + 'latest_value': None, + 'registered': False + } - # Add to setpoint list - msmt.setpoint_list[msmt.action_indices] = setpoint_info + # Add to setpoint list + msmt.setpoint_list[msmt.action_indices] = setpoint_info - # Add to measurement actions - assert msmt.action_indices not in msmt.actions - msmt.actions[msmt.action_indices] = self + # Add to measurement actions + assert msmt.action_indices not in msmt.actions + msmt.actions[msmt.action_indices] = self - return setpoint_info + return setpoint_info def exit_sweep(self): msmt = running_measurement() @@ -1419,7 +1419,21 @@ class Sweep(BaseSweep): sequence_keywords = ['start', 'stop', 'around', 'num', 'step', 'parameter', 'sequence'] base_keywords = ['delay', 'initial_delay', 'name', 'label', 'unit', 'revert', 'parameter'] - def __init__(self, *args, start=None, stop=None, around=None, num=None, step=None, delay=None, initial_delay=None, name=None, label=None, unit=None, revert=None): + def __init__( + self, + *args, + start: float = None, + stop: float = None, + around: float = None, + num: int = None, + step: float = None, + delay: float = None, + initial_delay: float = None, + name: str = None, + label: str = None, + unit: str = None, + revert: bool = None + ): kwargs = dict( start=start, stop=stop, @@ -1527,8 +1541,7 @@ def _transform_args_to_kwargs(self, *args, **kwargs): sequence_kwargs = {key: kwargs.get(key) for key in self.sequence_keywords} base_kwargs = {key: kwargs.get(key) for key in self.base_keywords} - print(f'{sequence_kwargs=}') # TODO removeme - print(f'{base_kwargs=}') # TODO removeme + return sequence_kwargs, base_kwargs def _generate_sequence(self, start=None, stop=None, around=None, num=None, step=None, parameter=None, sequence=None): diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 619f22f5817..f7f7e907949 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -25,7 +25,11 @@ def func_context_manager(): db_path = Path(temporary_folder.name) / "test_database.db" initialise_or_create_database_at(str(db_path)) - yield load_or_create_experiment("test_experiment") + try: + exp = load_or_create_experiment("test_experiment") + yield exp + finally: + exp.conn.close() return func_context_manager @@ -36,9 +40,9 @@ def test_original_dond(create_dummy_database): p1_get = ManualParameter("p1_get", initial_value=1) p2_get = ManualParameter("p2_get", initial_value=1) - p1_set = ManualParameter("p1_set") + p1_set = ManualParameter("p1_set", initial_value=1) dond( - LinSweep(p1_set, 0, 1, 101), + p1_set, 0, 1, 101, p1_get, p2_get ) @@ -55,7 +59,7 @@ def test_basic_1D_measurement(create_dummy_database): p1_set = ManualParameter("p1_set") with MeasurementLoop("test") as msmt: - for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + for val in Sweep(p1_set, 0, 1, 11): assert p1_set() == val p1_get(val + 1) msmt.measure(p1_get) @@ -79,9 +83,9 @@ def test_basic_2D_measurement(create_dummy_database): p2_set = ManualParameter("p2_set") with MeasurementLoop("test") as msmt: - for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + for val in Sweep(p1_set, 0, 1, 11): assert p1_set() == val - for val2 in Sweep(LinSweep(p2_set, 0, 1, 11)): + for val2 in Sweep(p2_set, 0, 1, 11): assert p2_set() == val2 p1_get(val + 1) msmt.measure(p1_get) @@ -111,7 +115,7 @@ def test_1D_measurement_duplicate_get(create_dummy_database): p1_set = ManualParameter("p1_set") with MeasurementLoop("test") as msmt: - for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + for val in Sweep(p1_set, 0, 1, 11): assert p1_set() == val p1_get(val + 1) msmt.measure(p1_get) @@ -139,11 +143,11 @@ def test_1D_measurement_duplicate_getset(create_dummy_database): p1_set = ManualParameter("p1_set") with MeasurementLoop("test") as msmt: - for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + for val in Sweep(p1_set, 0, 1, 11): assert p1_set() == val p1_get(val + 1) msmt.measure(p1_get) - for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + for val in Sweep(p1_set, 0, 1, 11): assert p1_set() == val p1_get(val + 0.5) msmt.measure(p1_get) @@ -174,11 +178,11 @@ def test_2D_measurement_initialization(create_dummy_database): p2_set = ManualParameter("p2_set") with MeasurementLoop("test") as msmt: - outer_sweep = Sweep(LinSweep(p1_set, 0, 1, 11)) + outer_sweep = Sweep(p1_set, 0, 1, 11) for k, val in enumerate(outer_sweep): assert p1_set() == val - for val2 in Sweep(LinSweep(p2_set, 0, 1, 11)): + for val2 in Sweep(p2_set, 0, 1, 11): assert p2_set() == val2 p1_get(val + 1) msmt.measure(p1_get) @@ -202,7 +206,7 @@ def nested_measurement(): p1_set = ManualParameter("p1_set") with MeasurementLoop("test") as msmt: - for val in Sweep(LinSweep(p1_set, 0, 1, 11)): + for val in Sweep(p1_set, 0, 1, 11): assert p1_set() == val p1_get(val + 1) msmt.measure(p1_get) @@ -213,7 +217,7 @@ def nested_measurement(): p2_set = ManualParameter("p2_set") with MeasurementLoop("test") as msmt: - for val2 in Sweep(LinSweep(p2_set, 0, 1, 11)): + for val2 in Sweep(p2_set, 0, 1, 11): assert p2_set() == val2 nested_measurement() From d4e18c14df16941825ba34263c9b9aa02aa1492e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 19:28:19 +0000 Subject: [PATCH 032/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 60 +++++++++---------- .../test_measurement_loop_sweep.py | 24 ++++---- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 68a1e149468..58f45fcc7b8 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1264,7 +1264,7 @@ def __iter__(self): "Cannot create a Sweep while another measurement " "is already running in a different thread." ) - + msmt = running_measurement() if msmt is None: raise RuntimeError("Cannot start a sweep outside a Measurement") @@ -1326,7 +1326,7 @@ def __next__(self): # Set parameter if passed along if self.parameter is not None and self.parameter.settable: self.parameter(sweep_value) - + # Optional wait after settings value if self.initial_delay and self.loop_index == 0: sleep(self.initial_delay) @@ -1378,10 +1378,10 @@ def exit_sweep(self): raise StopIteration def execute( - self, - name: str = None, - measure_params: Iterable = None, - repetitions: int = 1, + self, + name: str = None, + measure_params: Iterable = None, + repetitions: int = 1, sweep: Union[Iterable, AbstractSweep] = None ): # Get "measure_params" from station if not provided @@ -1418,20 +1418,20 @@ def execute( class Sweep(BaseSweep): sequence_keywords = ['start', 'stop', 'around', 'num', 'step', 'parameter', 'sequence'] base_keywords = ['delay', 'initial_delay', 'name', 'label', 'unit', 'revert', 'parameter'] - + def __init__( - self, - *args, - start: float = None, - stop: float = None, - around: float = None, - num: int = None, - step: float = None, - delay: float = None, - initial_delay: float = None, - name: str = None, - label: str = None, - unit: str = None, + self, + *args, + start: float = None, + stop: float = None, + around: float = None, + num: int = None, + step: float = None, + delay: float = None, + initial_delay: float = None, + name: str = None, + label: str = None, + unit: str = None, revert: bool = None ): kwargs = dict( @@ -1440,7 +1440,7 @@ def __init__( around=around, num=num, step=step, - delay=delay, + delay=delay, initial_delay=initial_delay, name=name, label=label, @@ -1450,11 +1450,11 @@ def __init__( sequence_kwargs, base_kwargs = self._transform_args_to_kwargs(*args, **kwargs) - self._explicit_sequence = None + self._explicit_sequence = None self.sequence = self._generate_sequence(**sequence_kwargs) super().__init__(sequence=self.sequence, **base_kwargs) - + def _transform_args_to_kwargs(self, *args, **kwargs): """Transforms sweep initialization args to kwargs. Allowed args are: @@ -1541,13 +1541,13 @@ def _transform_args_to_kwargs(self, *args, **kwargs): sequence_kwargs = {key: kwargs.get(key) for key in self.sequence_keywords} base_kwargs = {key: kwargs.get(key) for key in self.base_keywords} - + return sequence_kwargs, base_kwargs def _generate_sequence(self, start=None, stop=None, around=None, num=None, step=None, parameter=None, sequence=None): """Creates a sequence from passed values""" # Return "sequence" if explicitly provided - if sequence is not None: + if sequence is not None: return sequence # Verify that "around" is used with "parameter" but not with "start" and "stop" @@ -1556,7 +1556,7 @@ def _generate_sequence(self, start=None, stop=None, around=None, num=None, step= raise SyntaxError('Cannot pass kwarg "around" and also "start" or "stop') elif parameter is None: raise SyntaxError('Cannot use kwarg "around" without a parameter') - + # Convert "around" to "start" and "stop" using parameter current value center_value = parameter() if center_value is None: @@ -1573,7 +1573,7 @@ def _generate_sequence(self, start=None, stop=None, around=None, num=None, step= raise ValueError('Parameter must have initial value if start is not explicitly provided') else: raise SyntaxError('Must provide either "around" or "stop"') - + if num is not None: sequence = np.linspace(start, stop, num) elif step is not None: @@ -1581,7 +1581,7 @@ def _generate_sequence(self, start=None, stop=None, around=None, num=None, step= step = abs(step) if stop > start else -abs(step) sequence = np.arange(start, stop, step) - + # Append final datapoint if abs((stop - sequence[-1]) / step) > 1e-9: sequence = np.append(sequence, [stop]) @@ -1602,7 +1602,7 @@ def __init__(self, repetitions, start=0, name='repetition', label='Repetition', def measure_sweeps(sweeps: list[BaseSweep], measure_params: list[_BaseParameter], msmt: MeasurementLoop = None): """Recursively iterate over Sweep objects, measuring measure_params in innermost loop - + Args: sweeps: list of BaseSweep objects to sweep over measure_params: list of parameters to measure in innermost loop @@ -1612,10 +1612,10 @@ def measure_sweeps(sweeps: list[BaseSweep], measure_params: list[_BaseParameter] for _ in outer_sweep: measure_sweeps(inner_sweeps, measure_params, msmt=msmt) - + else: if msmt is None: msmt = running_measurement() for measure_param in measure_params: - msmt.measure(measure_param) \ No newline at end of file + msmt.measure(measure_param) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index 3dd61b77294..0b61e7f9905 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -73,41 +73,41 @@ def test_sweep_2_args_parameter_stop(): sweep_parameter.sweep_defaults = {'num': 21} sweep = Sweep(sweep_parameter, 10) assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) - + def test_sweep_2_args_sequence_name(): sweep_values = [1, 2, 3] with pytest.raises(AssertionError): sweep = Sweep(sweep_values) - sweep = Sweep(sweep_values, 'sweep_values') + sweep = Sweep(sweep_values, "sweep_values") assert np.allclose(sweep.sequence, sweep_values) - + def test_sweep_3_args_parameter_start_stop(): sweep_parameter = ManualParameter('sweep_parameter') - + with pytest.raises(SyntaxError): sweep = Sweep(sweep_parameter, 0, 10) - + sweep = Sweep(sweep_parameter, 0, 10, num=21) assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) - + sweep_values = [1, 2, 3] with pytest.raises(AssertionError): sweep = Sweep(sweep_values) - sweep = Sweep(sweep_values, 'sweep_values') + sweep = Sweep(sweep_values, "sweep_values") assert np.allclose(sweep.sequence, sweep_values) - + def test_sweep_4_args_parameter_start_stop_num(): sweep_parameter = ManualParameter('sweep_parameter') sweep = Sweep(sweep_parameter, 0, 10, 21) assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) - - + + def test_sweep_step(): sweep = Sweep(start=0, stop=10, step=0.5) assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) @@ -116,9 +116,9 @@ def test_sweep_step(): sweep = Sweep(start=0, stop=9.9, step=0.5) assert np.allclose(sweep.sequence, np.append(np.arange(0, 9.9, 0.5), [9.9])) - + def test_error_on_iterate_sweep(): sweep = Sweep([1,2,3], 'sweep') with pytest.raises(RuntimeError): - iter(sweep) \ No newline at end of file + iter(sweep) From ae0ee845a02119edfb3c11ec84ca0ed95813e878 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 2 Aug 2022 20:53:22 +0200 Subject: [PATCH 033/122] Wrote ~2/3 of MeasurementLoop tutorial --- docs/examples/DataSet/MeasurementLoop.ipynb | 892 ++++++++++++++++++++ 1 file changed, 892 insertions(+) create mode 100644 docs/examples/DataSet/MeasurementLoop.ipynb diff --git a/docs/examples/DataSet/MeasurementLoop.ipynb b/docs/examples/DataSet/MeasurementLoop.ipynb new file mode 100644 index 00000000000..35f32f10a56 --- /dev/null +++ b/docs/examples/DataSet/MeasurementLoop.ipynb @@ -0,0 +1,892 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# MeasurementLoop" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook introduces the `MeasurementLoop`, which is one of three methods used to perform measurements. The three measurement methods are in increasing levels of complexity:\n", + "\n", + "- `qcodes.dataset.do_nd.dond` and its variations `do1d` and `do2d`. \n", + " These are function calls that perform basic N-dimensional sweeps, measuring a list of parameters in the innermost loop. \n", + " It is a wrapper around the `Measurement` class\n", + "- `qcodes.dataset.measurement_loop.MeasurementLoop` can perform more complex measurements, including conditional measurements, nested measurements. It can perform arbitrary python code in a measurement. \n", + "The `MeasurementLoop` relies on a fixed order in which parameters are measured (examples below). This fixed order reduces the amount of explicit code needed.\n", + "It is a wrapper around the `Measurement` class\n", + "- `qcodes.dataset.measurements.Measurement` is the most explicit type of measurement. \n", + "All parameters that are swept / measured must be explicitly registered before the measurement starts, as well as preferably their array shapes. \n", + "The `Measurement` can perform arbitrary python code. It further allows parameters to be measured in arbitrary order.\n", + "\n", + "The `MeasurementLoop` therefore lies in complexity between the `dond` and `Measurement`, and should be able to meet the majority of measurement needs while minimizing the amount of explicit definitions. For example, in contrast to `Measurement`, the `MeasurementLoop` does not need any parameters to be registered.\n", + "\n", + "We will start with basic examples of the `MeasurementLoop` and then go over some more advanced features " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Basic measurement" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import time\n", + "\n", + "from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep\n", + "from qcodes import (\n", + " ManualParameter, \n", + " Parameter,\n", + " initialise_or_create_database_at, \n", + " load_or_create_experiment\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We start by creating a set parameter and a get parameter that returns a random value" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "random_parameter()=0.9198744153995131\n" + ] + } + ], + "source": [ + "set_parameter = ManualParameter('set_parameter')\n", + "random_parameter = Parameter('random_parameter', get_cmd=np.random.rand)\n", + "print(f'{random_parameter()=}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now perform a basic measurement: sweeping one parameter (`set_parameter`) and measuring another (`random_parameter`):" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 8. \n", + "Finished measurement\n" + ] + } + ], + "source": [ + "with MeasurementLoop('basic_measurement') as msmt:\n", + " for val in Sweep(set_parameter, start=0, stop=10, num=11):\n", + " msmt.measure(random_parameter)\n", + "\n", + "print('Finished measurement')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's break this code down line-by-line.\n", + "```Python\n", + "with MeasurementLoop('basic_measurement') as msmt:\n", + "``` \n", + "> Here the `with` statement instantiates the `MeasurementLoop` inside a context manager. Everything inside this block is part of the measurement. We use the variable `msmt` to refer to the instantiated `MeasurementLoop` as we will use it to measure parameters later on. \n", + "> We're also supposed to give the measurement a name, in this case `'basic_measurement'`\n", + "\n", + "```Python\n", + "for set_val in Sweep(set_parameter, start=0, stop=10, num=11):\n", + "```\n", + "> We use a `Sweep` object to register in the `MeasurementLoop` that we want to sweep `set_parameter` with 11 points spaced between 0 and 10. The `MeasurementLoop` now also knows that everything inside this loop has a dimension of (11, ). \n", + "> Notice that we are using a standard python loop, so we can access the iterated values `set_val`. \n", + "> Notice also that the value of `set_parameter` is being updated as we sweep over it.\n", + "\n", + "```Python\n", + "msmt.measure(random_parameter)\n", + "```\n", + "> Here we measure `random_parameter` inside the sweep. The `MeasurementLoop` automatically registers `random_parameter` once it's measured for the first time." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Comparison to dond" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since this is a very basic measurement, it can also performed in less code using `dond` as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 10. Using 'qcodes.dataset.dond'\n" + ] + } + ], + "source": [ + "from qcodes.dataset.do_nd import dond, LinSweep\n", + "dataset = dond(LinSweep(set_parameter, 0, 10, 11), random_parameter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It can even be performed by the simpler do1d:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 11. Using 'qcodes.dataset.do1d'\n" + ] + } + ], + "source": [ + "from qcodes.dataset.do_nd import do1d\n", + "dataset = do1d(set_parameter, 0, 10, 11, 0, random_parameter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At this point you might wonder what the use is of `MeasurementLoop`. The point is that the `MeasurementLoop` can also perform significantly more complex types of measurements, as we will go into later." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Comparison to `Measurement`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The same basic measurement can also be performed by the `Measurement` class as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 12. \n" + ] + } + ], + "source": [ + "from qcodes.dataset.measurements import Measurement\n", + "context_meas = Measurement(name='basic_measurement_Measurement_class')\n", + "\n", + "# Register the independent parameter...\n", + "context_meas.register_parameter(set_parameter)\n", + "# ...then register the dependent parameter\n", + "context_meas.register_parameter(random_parameter, setpoints=(set_parameter,))\n", + "\n", + "with context_meas.run() as datasaver:\n", + " for set_v in np.linspace(0, 10, 11):\n", + " set_parameter(set_v)\n", + " get_v = random_parameter()\n", + " datasaver.add_result((set_parameter, set_v),\n", + " (random_parameter, get_v))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are some clear differences with the code of the `MeasurementLoop`. - All set/get parameters involved in the measurement need to be registered beforehand, as well as their relation.\n", + "- When a parameter is swept over, it needs to be explicitly set.\n", + "- Any parameter that is measured also needs to be added, along with the corresponding set value(s).\n", + "\n", + "These differences all make the `Measurement` more explicit than both the `MeasurementLoop` and `dond/do1d/do2d`. On the one hand, this makes it more cumbersome to write a measurement. But on the other hand, this allows greater flexibility. For example, we could have set `set_parameter` to another value instead of the iterated value `set_v`; this would not have been possible using the other methods.\n", + "\n", + "The `MeasurementLoop` is supposed to lie somewhere in between `dond` and `Measurement`, enabling a wide variety of measurements while requiring a minimal amount of explicit code to be written." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## A more complex measurement example" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we perform a slightly more complex measurement. We perform a 2D sweep and again measure `random_parameter` inside it. However, we now count the number of times that it is above 0.5. Each time it is above 0.5, we sleep for 100 ms. after each inner loop we register how many times it was above 0.5." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 19. \n" + ] + } + ], + "source": [ + "with MeasurementLoop('conditional_measurement_example') as msmt:\n", + " for set_val1 in Sweep(set_parameter, np.logspace(1, 3, 51)):\n", + " above_half = 0 # We initialize a counter here\n", + "\n", + " # Notice that we don't need to sweep a parameter\n", + " for set_val2 in Sweep(np.arange(10), 'inner_set_parameter'):\n", + " random_val = msmt.measure(random_parameter)\n", + "\n", + " # We increment the counter if the random_val is above 0.5\n", + " if random_val > 0.5:\n", + " above_half += 1\n", + " # Let's also sleep a bit\n", + " time.sleep(0.1)\n", + "\n", + " # Notice that we don't need a parameter to measure this\n", + " msmt.measure(above_half, 'above_half')" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXkAAAEXCAYAAABI/TQXAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAABSCUlEQVR4nO2dd5wcd3n/38/u3u71IumKuixZxSquMpZxL1i2qaEFU+0ADimUFFryI5gQICRAIIEETLBJgJiAAUPASDbggovcLVm9Wbba3UknXW9bnt8fM7M3u7f1bud2b+/7fr32dbszszPf2bn97DOf7/N9vqKqGAwGg6E88RW7AQaDwWDwDiPyBoPBUMYYkTcYDIYyxoi8wWAwlDFG5A0Gg6GMMSJvMBgMZYwRecO0R0T+RkT+s9jtKAVEZKOI3FPsdhQbEblNRL6f47Y/EZEbvG5TsTAiX0BE5JCIDIlIv4i0i8h3RaR2Co57VESqRORqEflp0rrPisgLIhIRkduS1omI/K2IvCwivSLyQxGpz/GYS0RE7XN1P/6wgKeWE6r6eVV931QfV0RuFpFHsmzzoIhMum0icqWIHMlh088B/+h6n4rImQU4fs6iOQ35IvAPxW6EVxiRLzyvVdVa4FzgPOCTXh5MRBYCXao6BFwAPJu0yX7gY8CvUrz93cC7gEuAeUAV8G95NqFRVWtdj//N8/2TQkQCU3m8UkZELgQaVHVLsdsynVDVJ4F6EVlf7LZ4gRF5j1DVdmAzltinjMTsyP9a+/ltIvIjEflvEekTkR05/tOtB55xPU8QeVX9L1X9NdCX4r2vBb6jqodVtR8rovlDEanO/UzHIyJBEXleRD5ov/aLyKMi8nf269tE5G4R+V/7XJ8VkXNc759n30KfEJEXReRDrnXOe78vIr3Aze4o03WHcYuIHBaR0yLyARG5UES2iUi3iHw9qb1/JCK77G03i8hi1zq137/Pfu837Dugs4BvAhfbdzDdKT6HzwGXAV+3t/m6vXyViNwvIqdEZI+IvNX1nhtFZKf9uRwVkb8WkRrg18A81x3TvBQf/Q3AQ659PWw/3eq+yxKR19jXp1tEHhORs13v+bh93D67bdeIyPXA32D9b/SLyNYMl9+5wzlo7+NFEXmHvXyZiPxORLpE5KSI/EBEGl3vOyQiH7Wv04CIfEdEWkXk1/a+fiMiTUnX+VYROSYix0XkrzO0aYN9rt0islVErkza5EHg1ZnOa9qiquZRoAdwCLjWfr4AeAH4mv36SuBIhu1vA4aBGwE/8AVgS4ZjfRrott8zaD+PAj32c3/S9t8HbktadjfwMdfrSwAFzsnhXJfY2wbSrF8LnAbOAv4W2OK0yT7XMPBmoAL4a+BF+7kP60fr74AgsBQ4CGxMeu8b7G2r7GXfT2rXN4FK4Dr7M7oHaAHmA53AFfb2r8e62zkLCAD/D3jMdR4K/BJoBBYBJ4Dr7XU3A49k+ZweBN7nel0DHAZusY93HnASWG2vPw5cZj9vAs5P9/+T4lg/Bj6atEyBM12vz7PP/yKs/7P3YP0fhoCVdtvmuT7LZa7P/fs5/F/UAL3ASvv1XGCN/fxM4FX2sZqBh4GvJn0ftgCtruv0rN3mSuB3wKeTrvNd9jHX2dfm2uT22vvqwvpu+ew2dAHNrmP/JfDTYmuIFw8TyReee0SkD+vL0oklxrnyiKreq6pR4HvAOek2VNXPAHOwxPEMrH/gTaraoKqN9j6ysQl4nx0VNQAft5fnE8mftKMj53GW3b7tWD7nPVgi/q6kNj2jqnerahj4CtaXeANwIdaX7+9VdVRVDwLfBt7meu/jqnqPqsbUsqlS8VlVHVbV+4AB4C5V7VTVo8DvsYQD4APAF1R1l6pGgM8D57qjeeAfVbVbVV8GHsC+O5sgrwEOqeqdqhpR1eeAnwBvsdeHgdUiUq+qp1U12X7LRCOp79jc3Ap8S1WfUNWoqv4XMIL12UexBHi1iFSo6iFVPZDH8R1iwFoRqVLV46q6A0BV96vq/ao6oqonsK77FUnv/TdV7XBdpydU9TlVHQZ+xth1c/iMqg6o6gvAncBNKdrzTuBe+7sVU9X7gaexvjMOfVifX9lhRL7wvEFV67Air1VYQpwr7a7ng0ClpPCcReRc2x44jRUd7cESnyttoX1jjse7AysSehDYYe8DIJcOPoc59o+K89jlWvdfwGKsL9i+pPcddp6oasw+5jx7+3nuHw4sq6A11Xsz0OF6PpTitdMhvhj4mutYpwDBiv4ckq/LZDrTFwMXJZ3fO4A2e/2bsMTnJRF5SEQuzmPfp4G6HI7/V0nHX4gVve8HPoIVBXeK1RGfyhZKi6oOAH+I9eN5XER+JSKrAGzr5Ye2HdSLdXeZ/P3I9bo5uP8XXsL6H0pmMfCWpHO+FOsuw6EO6w647DAi7xGq+hDwXeBL9qIBXBGyiPixblknsu/nVbURK5Pi7+znO7FslkZV/Wmm97v2E1PVT6vqElVdgCX0R+1HIfh3LKtjo4hcmrRuofNERHxY9tYxrC/ti0k/HHWq6o66Clk69TDwx0nHq1LVx3J4by7tSN7mMPBQ0vFqVfVPAFT1KVV9PZa1dA/wozyOtQ1YkWWbw8Dnko5frap32cf/H1W9FEsYFaufJtfjY+9js6q+CktEd2PdiYF1l6TAOlWtx4qwJdf9pmGh6/kirP+hZA4D30s65xpV/UfXNmcBGfsapitG5L3lq8CrxOpU3IsVmb9aRCqwvN/QJPd/AfCsiAQZi8QSEJEKEanEutYBEam0f2AQkVl2Z5iIyGqs2+e/tyNrp5PzwYk0TETeZbfvZuBDwH9JYjrpBSLyRvtO5SNYlsEW4Emgz+4ArBKr03atWJkjXvBN4JMissZud4OIvCXLexw6gAX2559pm6Wu178EVojIu+xrUyFWp/BZYnVYv0NEGmwbqxfL+nD2M9u21dJxL+Ptj+Tjfxv4gIhcZF/3Gvt/sk5EVoqVhhvC6scYSjr+EvsHOS12tP56sTqLR4B+1z7q7Nc9IjIf+GimfeXIp0Sk2r5+twCpsru+D7xWrDEEfvs7cKWILHBtcwVW53bZYUTeQ2zf8b+xou0e4E+B/8SKlAfIzxZJhZMyuQ7Ynmabb2N9WW/C6gAdwkqbBOtW+V67Lb8G7lDV213vXQg8mqUN3ZKYJ/+XIrII6wfu3arar6r/g+WB/ovrfT/Huq0/bbfnjaoatn3712D53i9idUr+J5BJ3CaMqv4MK1r9oW0hbMfKUsmF32Hd/bSLyMk023wNeLNYmTv/qqp9WJ3Bb8OKOtvt4zs/+O8CDtlt+QCWlYOq7say1g7alsM4W8L273tE5CLX4tuwfmC7ReStqvo08H7g61if/X6sH2LsNvwj1mfejnU34aQA/9j+2yUimfoJfFidmMewrK8rgD+x130GOB8rOeBXQE53nFl4yD6H3wJfsvtgElDVw1gd7H+D1Tl7GOsHxgfx1NN+tVIpyw5RNZOGGFIjIs8D16hqV4H3extWxsc7C7lfA4jIdcCfquobit0WLxGRJdgZWXaH+WT29ROsVOJ7C9G2UsMMJDGkRVXPLXYbDPlhR7LjollDelT1TcVug5cYkTcYDHkjIv1pVt2gqr+f0sYYMmLsGoPBYChjTMerwWAwlDFG5A0Gg6GMKSlPfs6cObpkyZJiN8NgMBimFc8888xJVU05uLKkRH7JkiU8/fTTxW6GwWAwTCtE5KV064xdYzAYDGWMEXmDwWAoY4zIGwwGQxljRN5gMBjKGM9FXkT+Qqyp7LaLyF12RUSDwWAwTAGeirxdTvRDwHpVXYs13djbMr/LYDAYDIViKuyaAFBl1w2vJnVRf4PBYDB4gKcib8/T+CXgZawJintS1XueCq7/6sP84Im0qaRp+dW243zwruc8aJHBYDB4j9d2TRNWsf4zsOZerBGRdyZtc6uIPC0iT584ccKztuxu7+Nvf5ZuXo30PHXoFPftaM++ocFgMJQgXts112LN13nCns7sp8Ar3Ruo6u2qul5V1zc3T2jKU0+JxpTRaAxTrdNgMExHvBb5l4EN9hyMAlwD7PL4mOOIxiYu0JGYogrhqBF5g8Ew/fDak38CuBtrHtIX7OPdnvFNHhCJxbJvlO69Ueu9o9GJ78NgMBiKhecFylT108CnvT5OJiKTiMKdu4CRcJTaUEnVczMYDIaszIgRr5FJ2jVgInmDwTA9mRkiPwmBHovkjcgbDIbpx4wQeUeo/T7J+72On28ieYPBMB2ZESLvWC5+yV/kTSRvMBimMzND5O2OV98EznbMk48WskkGg8EwJcwMkbctl0lF8hETyRsMhunHDBH5SXjyUSPyBoNh+jIzRD46cZF3IvlRI/IGg2EaMjNE3rFrJpFdYyJ5g8EwHZkhIm93vE7CkzeRvMFgmI7MDJGfhF0TjnvyJrvGYDBMP2aGyNuWi4nkDQbDTGNmiPwkInnjyRsMhunMjBD5yZQ1MJG8wWCYzswIkQ9HHbsm//dGYsaTNxgM05cZIfImkjcYDDOVGSHyk0mhjBiRNxgM05gZIvITHwxlatcYDIbpzMwQ+clk1zhzvBqRNxgM0xBPRV5EVorI865Hr4h8xMtjpqIQI15NJG8wGKYjns5Mrap7gHMBRMQPHAV+5uUxUzGpKpRG5A0GwzRmKu2aa4ADqvrSFB4TGLNcJufJmxRKg8Ew/ZhKkX8bcJfXBxkYifC5X+1kODwmytE00//d/vABdrf3pt2Xqk55ds1vd3Vwz3NHp+RYhsnzk2eO8PPnzfUylC6e2jUOIhIEXgd8MsW6W4FbARYtWjTpY33zoQN8+/cv0lJXyfsvXwqMFRlzR/Kqyufv3U3vUIRVbfUp9+X8OMDU2TX/9rv99A6HecN586fkeIaJo6p84de76B+JcNEZs2lrqCx2kwyGcUxVJH8D8KyqdiSvUNXbVXW9qq5vbm6e9IEcQR+Njoly1ClQ5hJ5J0KPqpKOiEvkpyKSV1X2d/Zzsm/E82MZJs/+zn5O9o8yHI7xlfv3FLs5BkNKpkrkb2IKrBqAVAk0jvC7LXknSo/F0ot8YiTvvSd/vGeY/pEIvcMR0wcwDdhysAuAjWta+fEzRzJafwZDsfBc5EWkBngV8FOvj5WOaAohd6L0SAaRT4jko95H8ns7+uLPu/pHPT+eYXI8frCLeQ2VfPFNZ1MXCvCFe3cXu0kGwzg8F3lVHVDV2ara4/WxAJxgXV02TNi2a9zOjJNxk+oHwCE6xXbNvo7++PMTxrIpaVSVLQdPsWHpbBqrg3zw6uU8tPcEj+w7WeymGQwJlN2IV8eucQt61LZrYq6FcU8+YyQ/lno5FR2v+zrHIvmT/UbkS5l9nf2cGhhlw7LZALz7lYtZ0FTF5+/dldECNBimmvITecab8qksmWgOHa/ONtUV/imJ5Pd29LN4djVgIvlS5/EDlh9/8VJL5EMBPx/duJKdx3v5mUmBNZQQZSfyDm7pdiLylJF8NEMkb6+rDvk9j+SdzBpHNEwkX9psOdjF/MYqFjRVxZe99ux5nL2ggS/dtydhnIbBUEzKTuRT2jW2oKfy5DN1vMYj+WCAaEzj7/ECJ7NmzfwG6isDnDQdryVLLKY88aLlx4srncvnE/7mxrM43jPMdx55sYgtNBjGKD+RT7HMSaFMEPnYeJ8+mUhc5P2Atxk2TmbNipZa5tSFjF1Twuzt7LP8+KWzxq3bsHQ2157Vwn88eIAuczdmKAHKTuQd1GXYRFMIejSHFMpossh7aNk4mTXLW+torg1xwghEybLF9uM32NZaMp+4YRVD4Sj/+tt9U9ksgyEl5Sfy9u2zO0B35nh1y7mzLFMmhOPlVwet6g9e+vL7OvuYUxtkVk2QOXUh48mXMFsOnmJBUxULZ1WnXH9mSx1vu3AhP3jiZQ6e6E+5jcEwVZSdyKeya+Jpkil8ekfIU+F0vNaEvI/k93b0s7ylDsCK5I1dU5LEYsqWF7vSRvEOH7l2BaGAj3/aZModGIpL2Ym8Q2LUnilPPv0+nG2qKpxI3puMCSezZnlrLQDNdSH6hiMmQ6ME2dPRR/dgOJ4FlY7muhB/fMUyNu1o5+lDp6aodQbDeMpO5H0p0mucAmVu4Y/nyWeI5J1tnEjeK7vGyaxZ3mpF8nNqg4BJoyxFnPz4i1J0uibzvsvOoKUuxOfv3ZUwAttgmErKTuQdjY9lyaRxPPkMafJxK6fK445XJ7NmeYsVyc+pDQGYNMoSZMvBLhbOqmJBU2o/3k11MMBfXbeCZ1/u5tfb26egdQbDeMpP5FMsi6RIocwrkve443V/p9U5t8KO5JvrbJE3vnxJ4eTHZ7Nq3Lz5goWsbK3ji5t2m8ngDUWh7ETewZ1CGUlh1+RWu2ZqUij3doxl1sBYJG/SKEuLXe299AyFs3a6uvH7hE/cuIqXugb5/pYpn/nSYCg/kU814jUSH/HqEv5odpF3Sh54nUK5t6OfM22rBmC248mbSL6k2HLQ6kDNR+QBrlzRzCVnzuZff7ePnqGwF00zGNJShiI/3rBJVdbAsWmKHck7mTWOVQNWsauGqgrT8VpibDnYxeLZ1cxrrMq+sQsR4ZM3nEXPUJh/f3C/R60zGFJTdiLvkHsKZe4jXr1IoUzOrHGYUxs0dk0JEY0pTxzsYsMZ+UXxDmvnN/AH587nzkcPceT0YIFbZzCkp3xFPkXUnqrjNfPMUIkjXr2I5JMzaxya60Kc7DPZNaXCruO99A5H2LAse+pkOv5q40oAvnzf3kI1y2DIStmJfKo5XuPZNa5l4Vw8eSeS9zBPPjmzxmGOqV9TUjjzuebrx7uZ31jFey89g589d5TtR6dkojSDofxE3iExu2Z8x2s+nnyNx5G8O7PGYU5tyHS8lhBbDnaxZHY1cxvy8+OT+ZMrlzGrJmgGSBmmjLIT+fjMUClqx6fKuMlpZigPPfl9nYmZNQ7NdSH6Rkxpg1Ig6uTHL5t4FO9QX1nBh64+k8cOdPHgnhMFaJ3BkBnPRV5EGkXkbhHZLSK7RORib49n/U2VE5+q1HDGSN7+cQhV+BApfCSvquzv6B9n1YBVpAzMNIClwK7jvfQNRyZl1bh5+0WLWTK7ms/fu8vTiWgMBpiaSP5rwCZVXQWcA+zy8mApR7zGJubJO++r8PkI+n2MFPgLebxnmL4UmTUAc+pM/ZpS4fEs9ePzJRjw8fHrV7Gvs58fP3OkIPs0GNLhqciLSANwOfAdAFUdVdVuL461v7Mv3olpH4sXTw5wxyMvcmpgNL7MIRdP3lnn9wuhgI+RsPWe4XCUB3Z3pnzP/Ts7co7499ntTc6sAWiurQQy16959uXTHO0eyulYudAzGOaBPanPq9CcGhjlsf0nPdn3Ewe7OFbAz2XLwS6Wzqmhtb6yYPu8fm0bFyxu4iv372VgJFKw/RoMyXgdyZ8BnADuFJHnROQ/RaTGvYGI3CoiT4vI0ydOTNyjvPYrD3PtVx5KGPH61d/s5e9/uTO+TUpPPodIPuATggF/fPq/e547yi3ffYrjPYlCsr+zn/f/99Pc+8LxnNp8+JSVL71kds24dU4kn86uicWUm+94kn8r4OxD33r4ALfc+RQvdQ0UbJ+ZjvXO7zxB33BhR4CGozFuvvMpPnr31oLsLxpTnnzxFBcVKIp3ELHmgz3RN8I3HjADpAze4bXIB4Dzgf9Q1fOAAeAT7g1U9XZVXa+q65ubmyd9QKfjVYGBkSgLmqrY+unreO058xJ9+jxSKP2+xEj+WM8wAN2DiQJ1etCKul/qym2wi9Op6pQydjO7xqlEmVrkD3UN0DscKaid84gdWU9Fh+Cu433E1PpbSPZ29DEUjvLo/q6CpCnuONZD30gk5Xyuk+WCxU288fz5fPv3BxPuQg2GQuK1yB8BjqjqE/bru7FE3zPckfxoNMbs2hANVRX4JM2I10wTeUedSN5HKOCLR/JOdN2fdJvtRKW5jmgcGrVEvrJivMgHAz4aqyvSRvIv2AJ2erAwkXDPYDi+z6mwbPa09wKw81hh88W3Hrb2F/T7+NbDBye9Pyc/Pp/Kk/nwyRvOorLCz6d/sd2kVBo8wVORV9V24LCIrLQXXQPszPCWwh0bZTQSJeS3TlFIU7smQ0F5ZxufWKI7YkfeJ/qsSH68yFuvD+cq8uEoFX6hwp/6MsypTT/X6wtHHJEvzKjYxw+eRBXOXtDA4we64j9AXtA9OEpHr3VeO471FnTfWw9301Rdwc2XLOHeF47HLbGJsuXgKZY219BSQD/eTXNdiI9uXMmj+7v4VY42n8GQD1ORXfNB4Acisg04F/i8lwdzFygLR5WKgPXaJ5JygFTGSD6mBHyCiKSO5IdTi/yR07l1+g2Fo1QGxkfxDs2ZRN6OunsKFMk/ur+LmqCfj1y7nJFILB7BesGedsuiqarwF17kj3Rz9oJGbrlkCT6B7zzy4oT3FYnGePLFUwXLqknHOy5azJp59Xz2lzvHBQ4Gw2TxXORV9Xnbcz9bVd+gqqe9PqZ1XCuvPehEyQLu+UEcKyZT7ZpoTPH7rB+JoMuT77RFPjkrwhH54z3DOeU/D4djVAbTi/ycutQTesdiGhfH7qFwQW7zH91/klecMYtXLptDVYXfU8vGqddz/do29nX2FWz8weBohL0dfZyzoIG5DVW87pz5/O9Thzk9MLG7nR3HeukfiXhm1Tj4fcJn37CWjt4RvvYbU9fGUFjKcMTrGKORGMGAY9ckZtDnNBjKjuTBKv87Go0Ri2k8uk7nyUdjynG7czYTw+EoVSn8eIc5tcGUKZSHugboH4mworWWaEzpHZ5c9Hese4iDJwe45Mw5VFb4ueTM2fxud6dnHvHu9j7qKwNcvaqFcFTjoj9ZdhzrJaZwzsJGAG69fClD4eiEJ+t4/GDu87lOlvMXNfG2Cxdyx6OH4nc6BkMhKD+Rj3e8KqPRGEHbDhnf8TqWJ59OzKIxJWDfCQQDPkYiUbqHwvGBVMki736di2UzNBqlsiL9JWiuC9E/EhnnjztWzWXLrWykyVo2j9pZNZecOQeAK1e2cOT0EAdOeJNKubejj1Vt9ayZVw/AzuOFsWy2Hu4G4OwFjQCsbKvjqpXNfPexQxMqD7HlYBfLmmtoqfPGj0/mY9evoq4ywKd+bjphDYWj/ETe9Xw0EqPCby0RScqTd3W4pgvmI7GYK5L3MRqJJdgnqewaZ/tcMmyGI9ki+dRplC8c6SEY8HHhEivCnGzn62MHuphTG2SlPfL2ypXWj8eDHlg2qsru9j5WtNWyZHYN1UE/Owvky2890sP8xqr4HLkAt16+jK6BUX7ybH4jSyPRGE8VqF5NrsyqCfKxjat48sVT3PP80Sk7rqG8KTuRd1CsFMqQbdek63iF9JZNJJroyY9GYnT2jdkwqeyaM+bUIAKHc47kM3e8wvi5Xl842sNZc+tptgdMTUbkVZVH9p/k4mVz8NnnuqCpmhWttZ748u29w/QNR1jZVo/PJ5w1t54dBUqj3Hq4m7MXNCQs27B0FucsaODbDx/MaM0l88LRHgZGo553uibztgsXcs7CRj73q91mqkBDQSg7kXeya5I7XkUSI/ZoLiKf4Mn7GHFF8gGf0D+SaAH0Dkdoqgkyt74yt0g+HKUqQ8erE5G67x6cTtd18+tprLZEPnlQVj7s7+znRN8Il56ZKGZXrWrhyRdPFTzbY7ftNzt3DWvm1bPzWC+xPAQ4FacHRnn51GDcj3cQEW69fBmHuga5f2d7zvub6Hyuk8XnE/7h9WvpGhjhX+43nbCGyVOGIj/23N3xCpKyrAGkT6OMxhS/PzmStwR30axq+pOG5PcPR6ivDLCgqTo3Tz5LCmUqu8bpdD17fiNNcZGfeCTvjHJ95bI5CcuvWml1ij6yr7D1ZfamEPmB0SgvTTKffeuRboBxkTxYWTyLZlXzzYcO5ux1bznYxfKW2vg1mErWLWjgHRct4r8fP1SwuxzDzKXsRN5BUcLRWHygkRWQu+waV4pjugFRViRvvT8U8Mcj+aoKPy31IQaSIvm+kTB1lRUsaKriSA6iNRyOZYzkZ9falShd0wA6na5r5zdQX2lNZjKZUa+P7rcmp144qzph+QWLm6gLBQruy+9p76OtvpKG6goA1syzRHmyYrbtSA8isG7+eJH3+4T3X3YGzx/u5qlD2TN4w9EYTx3yPj8+Ex+9bhVN1UE+dc/2Sd/lGGY2ZSfyTqAWjVkiHU+hTLJrcovkYyk8+RFa6kPUhiroS9HxWlcZYMGsatp7h7Pmfw+FM3vyFX4fTdUVnOgf6wdwOl2Xt9YS8PuorwxMOJKPRK1BT8lRvHPsy1bM4YE9hU2l3N3ex8q2sdLKy1trCfhk0p2vWw93s6y5lrrKipTr33zBQmbVBLn94QNZ9/XC0R4GR6NT2umaTEN1BZ+4YRXPvtzN3aYcsWESlKHIW4LkCKw7T15TTBoCY+mUyUSiY5580G+NeO3oHaa5NkRtyJ+QXaOq9A1HqA0FWNBURUyhPUuu/HCWFEpwpgFMjOTPmlsfv0NpqglOOJLfeqSH/pEIl545XuTBSqXs6B0pWBGxSDTG/hP9CSIfCvg5s6V2UiNfVZWtR3o4x06dTEVV0M+7L17Mb3Z1si9LXr4z2vcVZ3ifH5+JN52/gPWLm/jHTbsnZckZZjblJ/L2X6cEQdBl17gj+bDLrkmj8QkjXkO2GB89PWRF8pWBhE7J4XCMaEzjdg1kr2GTLYUSrM5Xx5N3Ol3PdlkSjdVBuieYheHUc08XsV65wkqlLFSWzaGuQUYjsbgf77BmXsOkRP5YzzAn+0c4Z+F4q8bNuy9eQmWFj2//PnPhsscPdLGitTh+vBufT/j716+le3CUf968p6htMUxfyk7kHZxiYmN2zQQi+VhiJA9WCmBLXSU1oUSRd0a71lUGWNhk+duZMmzC0RjhqGYV+Tm1oXgKpdPp6vadG6sqJhzlPbL/JGvm1Y+bRNyhpb6StfPrC+bLOyNb3ZE8WJ2vJ/tH6OzNPko4Fc4gqEyRPFh56G+5YCH3PHeMjjTHCkdjPH3otOelDHJl9bx63vPKJfzPky/Hz9NgyIesIi8iH7b/XuJ9cyaPo+MjkcRIXiT1vK+QayTvjy9rrgtRFwowGonFbSGntEBdZYC5DZX4fZIxw8YZgZmp4xUcu8YSeXenq0NTdcWE8uQHRyM893J3fJRrOq5a2cIzL50uSCG03e19+IRxE5c7I18nGs1vPdJNhV9YNXf8NIrJvO+yM4jEYtz56KGU67cd6WEoPPX58Zn4i1etYE5tiE/9fHteuf4GA+QWyd9i//03LxtSKJyvwEgkKZJPSqHMLZKPxcsahFzlgJvrQtSErMwWx5d3R/IBv4+2+sqMZW6HbJEP5WDXDIxGGRyNJHS6OjRWB+keyF+Anzp0mtFoLKvIX7myhZjCw/smP5HI3vY+lsyuGdfZvHqS5Q22Hu5m9dx6QhnSUR0Wz67hhrVz+cETL6UcA7AlXq+mdES+vrKC//fqs9h2pIcfPvVysZtjmGbkIvK7RGQfsFJEtrkeL9jlg0uKtB2vkjjHa0IKZZroKOoeDFWRKPK1tsg7QuH8dbI7Fs6qyhjJOxUts9s1Y2mULxztYbWr0xWgqTpI30gkoY8hFx7bf5IKv3DhkqaM2527sJHG6oqC+PJ7OvrGWTVgfWaLZ1dPKI0yGlO2H+2N16vJhVsvX0rfcIQfPjleMLcc7GJVW11aC6tYvO6ceWxYOot/2rSHLjO5uyEPsoq8qt4EXAbsB17rerzG/luSJNs1yR2vuaRQRtylhl3C2pJC5Ptcdg2QdUCUE8lnFXl71Gtn37A90jWxc7HRzjfPdwj8I/tPcv6iJqqDgYzb+X3CFSuaeWjPiUnlaw+HoxzqGmBFa2pLZfXc+gnZNQdP9NM/Ehk30jUT5yxsZMPSWXznkRcTfhxHI5YfX0pWjYOI8NnXr2VgJMIXN+0udnMM04icOl5VtV1Vz1HVl5IfXjcwXxy9diL5CnfHK4kdr87o2EiawVAZI/nKdHaNJboLmqro6BuO20bJjE39l/kSOPVrnjp0elynK4yJfD6dr6cGRtl5vDerVeNw9aoWugZG430CE2FfRz+qsCpFJA+WL/9S1yC9eU7svdWeIeucFCNdM/HHly/jeM8w/7f1WHzZtiPdth9f3NTJdCxvreO9l57Bj54+wjMvTcm0DIYyIJeO1xeSbJrStmtsIXdSKEPujtekSN4pXhZLE8mHo4rfHvEa9I+VLJ5dM+bJ96WJ5Bc2VaMKx7pTZ3EM5xjJO/VrHLtkbZLIN02gfs3jB7pQJWeRv3x5MyKTS6XcY2fWrEgr8tZ57cozmt96uJvaUIClzbXZN3Zx5cpmVrbWcfvDY6UO4n78GaUXyTt86JrltNVX8ql7tuc0MY3BkEsk79gyyY+StGuc/3vH807X8RqJxuIddelmh4q6Sg07+5ldG8Lvk7hdM5Ak8jVBx66xcuXTpVE6dk2mmaHASvsTgWdeOk0oqdMVxiL5fAZEPXrgJLWhQM7Rb1NNkPMWNvLAnol3vu5p7yUY8LFkdk3K9RPNsNl2pJu18+vjtlquiAjvv3wpu9v7eGivdV5bDp5iVVsdTSXmx7upCQX41GtWs/N474QnQzHMLHLx5MdZNKVs1ziTb4/LrhHGlRqOR/IZqlA6BcqcbVvsyDruyQ+PiXxtKBAXmwV2LZjDp1L78rlG8lZpgyDRmCaMdHVwIvl80igf3X+SDUtnxTOHcuGqlS1sO9Kdds7ZbOzp6Gd5S21aMW6pr2RObSivDJuRSJSdx3vz8uPdvO6cebTVV3L7wwcZiUR5+qWprR8/UW5c18Zly+fw5fv2ppwe0mBwk/O3XEQ2iMhTItIvIqMiEhWRrN9IETlkWzvPi8jTk2tudpyoPO7Jp+l4jbrq2qSP5HVcJO/YJzXjOl7DcasGoK2+koBPskfyWUQexjJsUhXfyteTP3xqkJe6BnO2ahyuWtWCKjy8d2LR/J723pSZNW7WzMuv83X38T7CUc06CCodwYCPP7p0CY8d6OIHW15mOBwryU7XZESEz7xuDcORKF+4d1exm2MocfIZ8fp14CZgH1AFvA/4Ro7vvUpVz1XV9Xm2L2+cdMh4WYM0tWvC0RwiedekIWkjeVcKpVvk/T5hXmP6NMrhHFMoYeyHJZXI14YCBHySsyf/2IHEqf5yZfXceprrQvxud/6+fPfgKB29I+PKGYw7xrx69nX0pe2sTsYpLzzRSB7gplcsoi4U4B837UYELipyvZpcWdpcy62XL+Wnzx3lCbsvwWBIRV5lDVR1P+BX1aiq3glc702zJo4TlTvzsLpTKBUrq2VgJEI0lujJO8vdpIrknfk+/T6hqsKf4Mk7wu+woKkqbf0aJ7smF5F3aqgkd7qCFdU1Vlfk7Mk/ur+L5roQy1vy66j0+YQrVzTz8N4TeXf4ORNT5xLJR2LKvo7+nPa79XAPc2qDzGuY+BysdZUVvH3DIkYjMc5qG5uIZTrw51ctZ35jFZ/6+fa8x0kYSov9nX1pS21MlnxEflBEgsDzIvJPIvIXOb5fgftE5BkRuXVCrcyD5IFN8UlDxOp4/dTPt/OnP3jW8uTt9MWoKh//yTY+dNdzCe+18uStbeoqKwj4hEWuuuvuImWWXZNY5nZ+YxXHulNH8mMjXrN/hAubqqkLBcZ1ujo0VgdztmuefPEUFy+dHZ9BKx+uWtVC73Ak7/S9dDVrksm3tvzWI92cs6BxQufi5o8uOYNgwMely/O7uyk2VUE/f/fa1ezt6OcXzx/L/gZDyfK5X+3ird963JMJ3PMR+XfZ2/85MAAsBN6Uw/suVdXzgRuAPxORy90rReRWEXlaRJ4+cWLyQ+eTrRefLQCODHT2jXC0e4ioq+M1GlXae4bHzU7kzq5pqKrg1x++jD84f358fW0oEJ8C0Kkl76a+qiLeMZvMSDiKyJgNlIkPXLmM//vgpeM6XR1yrV8zHI7S3jvMsjzTDR0uX9FMMODjvp0deb1vd3sf9ZUB2uozR9yLZ1VTGwrkVFu+bzjMgRP9k7JqHFrrK/n1hy/jw9csn/S+pprrVrcyv7GKX24zIj9d6R0O8+j+LjauaZt0wJKKnEXezqYZVtVeVf2Mqv6lbd9ke99R+28n8DPgFUnrb1fV9aq6vrm5Od/2jyPZXneSORyxj8ZiDI5EiEQ1btdEVRmJRMeNGo3ElIB/7ENf3lqXILQ1IX98CsC+kci4SL4m6GcwHE356zwUtsoM53JRa0MBlsxJnXoITiSf3a45bte3n2+nd+ZLbSjApWfOYdP29rwijr12OYNs52pN7F2XU+frC0d7UE093d9EWNZcG+9Mn06ICDeua+OR/SfNxN/TlAd2dzIajbFxTasn+88nu+YSEblfRPaKyEHnkeU9NSJS5zwHrgO2T67JmVHSRPKu0a0Do1EisdhYJB9TRiKxcV8StyefitpQID4FYHJ2DUBVMIDqWCerG0fkC4FVbjj7F9yxjuY1TtzD3rimlaPdQzlnwajquNmgMrF6bj27jmef2HvrYWeka2NO+y1nblw3l3BUuT/POyxDaXDfjg6a60KctzBzHamJko9d8x3gK8ClwIWuRyZagUdEZCvwJPArVd00kYbmSnKA6bNF2tHqaEwZGIkQUxJSKJ2ywU7+urPc8eRTUWvXlA9HYwyHY9QlRYI1IUvEB0bHWzZDo7Gc0idzwZodKrtdc9TO9FnQWJ1ly/Rce1YrPoH7drTntH177zB9w5GsmTUOa+Y1MDBq1bnJxLYj3SyaVV3SA5eminMXNjK/sYp7Xzhe7KYY8mQ4HOWBPZ1sXNMa16pCk4/I96jqr1W1U1W7nEemN6jqQbvmzTmqukZVPzfJ9mYl2UZwPjfHKojENJ6B49g1MTuSh8RCX7lE8v0jkXElDRyc4l+DI+NTAocj2af+y5XG6gpGkn6gUnGkewgRaJtENsrs2hAXLpnF5h25RY2745k19TltvzrHka9bD3cXxI8vB0SEG9a28ft9J/Ku/WMoLr/fd5LB0Sgb17R5doxcatecLyLnAw+IyD+LyMXOMnt5STHek08UaXfteCezJRLTeG62I/KqmjBpSCpqQgEGRiLxztVUnjykjuSHR6NZJwzJlVxHvR49PURrXeVYxtEE2bimjT0dfbx4MnO0Da70yRwj+RWtdVT4JaPId/YNc6xnOO+iZOXMjWdbls1vjGUzrdi0vZ36yoCng/By+bZ/2X5cBKwHPu9a9iXPWjZBkouNOSLv/HVXnHQPhkqO5J1oP2MkXxmgbyQSj55qkyN5274ZHB0fYQ+Fo1TmMMlFLjRW2fVrskwecrR7cMKdrm6uszuINudg2ext76OtvpKG6oqs24JloS1vqctY3mCb48ebSD7OuQsamdtQaSybaUQ4GuO3uzu4dnVr2sy5QpBL7ZqrMjyudrYTkfd41so8SI7kRRL/un8E3J58XOTtDkwn397vzyDyQWsKQCeCHm/XWCI+mCqSDxcukm+MV6LMHMkf6x5mXuPkRX5BUzVr59fnJPK72/vSVp5Mx5p59ew81pM2g2fbkW58MlbUzGD1Pd2wdi4P7z1pLJtpwpMvnqJ7MMz1Hlo1UNiJvD9cwH1Ngsx58omRvDNv69hcrflE8k7KnZOaWJ9k1zgiP5DCkx8KF7Lj1a5fkyGFLhZTjvcMMb8AIg9w/Zo2nnu5O+MovUg0xv4T/WlryKdj9bx6TvaP0pmm+NbzR3pY0VqXdcKTmcarz25jNBrjt7uMZTMd2LS9naoKP5evmHzqeCYKKfLedA3nSfJ0rcl58u5iZI5dM+TqsHREPmr/GGTMrrEj93Zb5JMjeafscNpIvmAplNk9+c6+EcJRLYhdA8Q7ijINjDrUNchoJJZ2Nqh0ZBr5qqpss0e6GhI5b2ETbfWV/GpbbplPhuIRiymbd7Rz5crmggV76SikyJfENPLpPHlxpVA6OCLv9szHInnr1yJbdg2MRfLJtWuq4ymUKSL50cJm10DmiUOOdlujeRcUKJI/s6WWpXNq2Lw9vaA45QzyjeTPmmttv+PoeF/+5VODdA+GjR+fAp9PuGFdGw/vOxGfqcxQmjx/pJvOvhGuX+utVQPlGMmn9eSdSN6dXeN45ikieceTz0nkrfzz8dk1Tgplikg+UrhIvrLCT1WFn9MD6SP5o92TG+2ajIhw3Zo2thzsivdjJLO7vQ+fWD8I+VBXWcGS2dUpO1+d6f4KNdK13Hj1urmMRmITqhZqmDo2b2+nwi9ctarF82MVUuQfLeC+Joy7s05kTNwdqU4dyY+JcG+SJ1+RoePV8eTbe4YJBXzjUhOrUvyIOAyNRrPOCpUPTdUVGT15ZyBUITpeHTauaSUSU367O7Vls7e9jyWzayZ0O7pmXkPKNMqth7sJBXw5j6CdaZy/qInW+hC/2maybEoVVWXTjnZeuWzOuH48L8inrEGriHxHRH5tv14tIu911qvqn3vRwHxxB/LuHPl4WYMc7ZqxSD79R+R48Md7hsdF8WDdPldV+Md58k7KZqFSKAEaslSiPNo9SENVxThLaTKcs6CR1vpQ2iybPR19efvxDqvn1fPyqfETe2870s2aeeNnyDJYOFk2D+49Ea+Qaigtdrf38VLX4JRYNZBfJP9dYDMwz369F/hIgdszadyevNtpiRcoS5EnP5TSk889u6ZnaHzdmrFt/OM8eSdds1AplOBUoswcyRcqs8bB5xM2rmnjob0nEj5DsDqWD3UNTDjidka+uitSRqIxXjjaY/z4LNxoWzYmy6Y02byjHRF41WpvCpIlk4/Iz1HVHwExAFWNALlN4TOFuD15yRLJO9GgI8KhgM8VyVtCnIsnD+Mzaxyqg4FxnvxQjvO75kNTdeb6NUe7hwrmx7vZuKaN4XAsPhm2w76OflSz15BPR6qJvfd29DMcjpnMmiysX9xES13IDIwqUTZtb+fCJbPikwF5TT4iPyAis7EdERHZAOQ2u8MUki6Sl3ip4bH1Ab+PgE8Ysu2U5rpQfpG8KxJPL/LjI3kvRL6xuiJtB6iqehLJA7zijFk0VFWMK1i2J8eJQtLRUldJc10oIZLfVoDp/mYClmXTxoN7Toyb7cxQXA6dHGB3e5+ntWqSyUfk/wr4BbBMRB4F/hv4oCetmgSaIPKuSN7+686uCfgEn0/innyLW+Sj2bNrAn5fPA2yLpS6A6UmFEhpZUBus0LlSqPd8ZpqlGjvUISB0SgLPIjkK/w+rjmrhd/s6kiYgm5Pey/BgI/FsyZe8dKa2Hssjth6pJv6ygBLZk98nzOFG9fNZcRk2ZQcTv+VV7XjU5HPpCHPAFcArwT+GFijqtu8athEcWtcqo5Xt53j9wmBBJGvjFdzjEfyGbJrAGptcU+uW+NgRfJJdk0e87vmSlN1kGhM6U0xE9URO0e+kJk1bjauaaN3OMITB0/Fl+3p6Gd5Sy2BSXSQrplXz/7O/njxuK2HLT/ei9lzyo31S2bRbCybkmPzjnbWzW9gQdPUBSr5ZNdsAz4GDKvqdlUtydEWmuDJjz1PrkYJVnqkXySe/dJSb3lkvUNhlyef+SOqtQc8ZbJrkksNO5F8ITtenfo1qSwbJ33SC7sG4PLlzVRW+BKybPa09+ZceTIdq+c2EIkpe9v7GRqNsqejz+TH54jfJ1y/po0H9nSmHHFtmHo6eod59uXuKcuqccgnzHotEAF+JCJPichfi8gij9o1YWJZ7Bo3fp8Pv38skm+2O0J6hsJxuyaTJw9jEXyqFEqwBkSNi+RtkS/kcOYme9Rrqs7Xo/aMUF50vIL1Y3XlihY272gnFlO6B0fp6B2ZdC77WOdrDzuP9xCNqel0zYMb181lOGwsm1LhviJYNZD/HK//pKoXAG8HzgZe9KxlEyTZjnFIFckHbLvGsU+cSL5nKJzTiFcYG9Vany6SD/nHDYZypgMsdMcrpBb5Y91DVFb4mO3hLEob17bS2TfC80e64zXk860+mcwie2LvHcd6ed6UF86bV5xhZXAYy6Y02LSjnWXNNZzZMrUD+fIaGSMii4E/tB9RLPumpNA02TWpQvmAX/CJxP33ljprxqSeoXA8vTJbJO/YNOkGGdUEA+MyHLyI5MfKDaewa7qHmNdY5amXffXKVgI+YfOO9nh9nHxr1iTj8wmr59az83gvvcNh2uoraa2f+KxWMw2/T7h+bSt3P3OEwdGIqdpZRLoHR9ly8BQfuGLplB87H0/+CeBngB94i6q+QlW/7FnLJkgsoaxBZrvGieQdmusmEMmHMts1VUE/I5FYQurm8GjhPfmmDDXlvUqfdNNQXcHFy2Zz344Odrf3UVcZoK0Agrx6njWx93Mvdxs/fgI4ls2De05k39jgGb/Z1Uk0plOaOumQjyf/blU9X1W/oKoH8zmIiPhF5DkR+WWe7cubxLIG7ufjxdrv8yVMntviEvmx2jWZP6IxkU8fyUNifZxhO1ukcpLT8Llx7KJUo16Pdnsv8gDXrWnjxZMD3Lezg1VtdQW5c1g9r57B0Sgvnxo0Vs0EuOiM2cypDfIrY9kUlU3b25nXUMm6+VMfqOSjMu0i8hURedp+fFlEcm3xh4FdE2hf3sSypFC6cUfyPoEm27PuSciuyWLXZBF5p9yw25cf8iCSD/h91FcGxkXyw+EoJ/tHp0TkN65uRQRO9I1MuGZNMu7Zn0yna/747dITv9vVOW68hmFqGBiJ8Pt9J9i4tq0o6b/5iPwdQB/wVvvRC9yZ7U0isgB4NfCfE2lgvqQbDOV+7gh3wC/xSD4Y8FHh91ET9CdE8tk8+VwjebcvH/fkC1igDKwfqeRI/pjHmTVuWuorOc+Otifrxzssb6mLVwJdZ+yaCXHjurkMhaM8uMdk2RSDh/aeYCQS83yav3TkI/LLVPXTqnrQfnwGyKUX4atYHbSxLNsVhFhSqeFUzxvsia8r7LIGMDYVYENVRV6evGOT1Fel9uTH5nl1RfLhKMFAolVUCBqrg+PKDcfTJ6cgkoexGaMKFckHAz5WtNaxdE5N/LoZ8uOiM2Yxq8ZYNsVi0/Z2ZtcEWb9kVlGOn093+5CIXKqqjwCIyCXAUKY3iMhrgE5VfUZErkyzza3ArQCLFk0+7d49/V8qHx7gdefMY/W8eubUhuLbOBUpG6qD9Ay68+Qz/w6+7tz5VIcC8cycZKrjnvyYyI+EYwVNn3RorKoYl0LpRR35TLxjw2KCAV9B/6Fve92ahLl5DfkR8PvYuKaNnz9/lOFw1PPp5gxjjESi/G53J685e27WgNEr8onk/wT4hogcEpGXgK9jlTfIxCXA60TkEPBD4GoR+b57A1W9XVXXq+r65ubJT2irpE6hdAv+nNogb12/EBgrW+DUkXEm34hH8lnKGsyqGdtXKsamAHTZNaOFmxXKjVVuOEnku4fwCbQ1TE3qYW0owC2XnFHQf+gLl8zi4mWzC7a/mcir181lcNRYNlPNYwe66B+JsHGKR7m6yWcw1POqeg7WIKh1qnpetto1qvpJVV2gqkuAtwG/U9V3TqrFWcil49Vtk/jtFUG/I/JWyd5wDnO85sLYFICJdk0hO10dGquDdA8k2TWnh2irrzSTbMxwNiy1LJt7XzCTfE8lm7e3UxcK8MoiBin55MnPFpF/BR4EHhCRr9mlh0uK5On/4s/J3Akb9+Ttkr25evLZcDx5dyQ/HI7G7aFC0lQdpG8kklAN0qs68obphWXZtPLbXR3x2kkGb4nGlPt2dnDVqpa4vhSDfJTmh8AJ4E3Am+3n/5vrm1X1QVV9TX7Ny590kbxbq/2pRD7JrgnnWLsmG072zWBSdo03kbzVMdnj6nydqhx5Q+lzw9q5DIxGx03wYvCGpw6d4tTA6JQXJEsmH5Gfq6qfVdUX7cc/AFNbaScH0hYoS2fX+BI7Xp2SvT22t124SH4sehoOe+PJOyLv5MpHY0p7z/CUdboaSpuLl82msbrC1LKZIjbvaCcU8HHFisn3NU6GfET+PhF5m4j47MdbseZ8LSnSlRqWdFF9PE9+LIUS4ES/JZTZsmuyEQr48EniPLJDHol8U1L9mo7eYSIxNXaNAbBShjeubuO3uzqNZeMxqsrm7e1cvqI5fjdfLLIqmIj0iUgv8H7gf4AR+/FD7NTHUiKxrEHq2jX+hEje+gjckTxAV//IuG0ngoiMKzc8HI55ksY2VonSEvmpzpE3lD43nj2X/pEIDxvLxlNeONrDsZ7hotSqSSbrT4yqxke1iMgsYDlQsqUAE0a8un7CJN3oV/upI/KOUJ60RX6ynjzY5YZHEssaeCHyzg+Uk0bpjHb1Yto/w/TklbZl8+vt7VxXAgJUrmza3o7fJ1x7Vkuxm5L7YCgReR9WDZoFwPPABuAx4BpPWjZB0nny6XLmxyJ5S3Sdkr0n+0fxCQUZlTo+ko9SFSx8dk2yJ39kigdCGUqfCr+P61a38usX2hmJRIua9VHObN7RzsVLZ8f1pJjkozQfBi4EXlLVq4DzgJ7Mb5l63CNeJU3Hqztl3HkeDIxl14Bl10zWj3dInjhkOBwteN0asAYiBXwS9+SPdg/RVF1h6ogbErhh3Vz6RiL8fu/JYjelLNnf2ceBEwNFHQDlJh8VG1bVYQARCanqbmClN82aOLE0k4aks2sCSZ680/E6MBot2KjN6opAvNSwqnqWQikiNFZXjHnyp02OvGE8lyybQ31lwGTZeMSm7daAs+tWl0byYT4if0REGoF7gPtF5OfAS140qlD403S8Jtg4SXnyAb8vXlGyEH48JEbyo9EYMS3srFBuGquDcbvmmMmRN6QgGPBx3Zo27t/ZwUjEZNkUmk072jl/UWPJzGKWT1mDP1DVblW9DfgU8B3gDR61Ky/cna3p8+TH58YD46pQwlgHZra6NbningJweLTw87u6cerXqKo9EKrak+MYpjevti2bR/YZy6aQHDk9yPajvUUfAOVmQqazqj6kqr9Q1fFzzRUBd258LE2efELHa4bBUDDWgVmwSD44FsnHZ4XyNJIP0z0YZnA0auwaQ0ouOdOxbEwtm0KyeUcHQEmkTjqURdUqd2582kie1Jk2fkkl8nYkXyCRrwmNRfJjs0J589E3VlXQPRh25ciXxi2jobQIBny8anUb9+9sZzQyJVM9zAg2b29nVVsdi2fXFLspccpD5F3C7o7q3ckxaWvX+FOIfJUTyRfm46lyRfLOrFCe2TU1VhVNJ33S2DWGdLz67DZ6hyM8ut9YNoXgRN8IT710qqSsGigXkXc9TxfJk86ukVSevCXyBYvkg34iMWU0EosPJw95ZtdUMBKJcfBkPzA10/4ZpieXnDmHulDAzBhVIH6zqwNVjMh7gTt61zQTCGUrNRxMYdcUzpN3ZoeKeB/J223fcayXqgp//AfLYEgmFPDzqtWt3LfDWDaFYNP2dhbPrmZlgaa+LBTlIfKkzq5xk1iUzP08Q8drobJrQmOVKIc9FnnHatpxtIf5TVVFmR3eMH24cd1cy7I5YCybydAzFOaxAye5fk1byX3nykLk3aSN5NMOhkrMkwdXCmWhRrwGx2rKDzkplB4MhoKxu5BDXYOmnIEhK5etsCybe7cZy2YyPLC7k3BUS2aUq5uyEPnEFMo0nbBpatf4UuTJe5FCCYmRvBdlDQCaasbsGTMQypCNUMDPtatbuW9nR8KMYob82Lyjndb6EOcuaCx2U8ZRFiLvJsGfd9k4ibVrxkfyqTz5gpU1SOHJV3qWQjlWEMlUnzTkwo3r5tp2Q1exmzItGRqN8uCeE1y3uq0gBQ0LTVmIfDphT7RuUts1vhR58k0FjuQdT35wZAo8+WoTyRvy47Llc6g1ls2EeXjfCYbC0ZLLqnHwVORFpFJEnhSRrSKyQ0Q+48VxEjteU2+TajYoSF3WwImGCx3JD4xGxuwaj0S+ssIf/wEx6ZOGXKis8HPNWS1s3tluLJsJsHl7O43VFbzijFnFbkpKvI7kR4CrVfUc4FzgehHZUOiDJKZQpo7k003/50uRXVNXGcAnhc+uGRyNMhSOEvAJFX7vPnrnTsR0vBpy5cZ1c+keDPO4sWzyIhyN8ZtdHVx7Vqun3+nJ4Gmr1KLffllhP9LE2pM4jut5LJ0n79rGl8WT9/mExupg4bJrKuxI3s6u8cqqcWioDuL3Ca11IU+PYygfrljRTE3Qb8oP58mWg130DkdKqlZNMp7PJiEifuAZ4EzgG6r6RKGP4Y7eTw2MupaPbZM45V/mAmVg5ZsXypN30iWdSL7So/RJh6bqCtrqKwmUaGRhKD0sy6aV/9t6jGM9w8VuTk601oX41GtXU19ZvAF/m7a3Ux30c9nyOUVrQzY8F3lVjQLn2rXofyYia1V1u7NeRG7FnhB80aJFEzpGOu/cHeFLmhTKS5fP4aZXLIznxju8c8PihHTEyRAM+Aj6fQyORhkJR6ms8FZ833zBgoQfO4MhF265ZAnHuofoHQoXuyk58dj+k7T3DnPHzRcWxSqJxZT7dnZw1coWz/rYCsGUzQunqt0i8gBwPbDdtfx24HaA9evXT8jKqQ4GuOv9G7jp21vSbpMg8q7/h1Vt9XzhjWeP2/6PLj1jIk1JizVxiJVC6bVd88bzF3i6f0N5ct6iJu7+k1cWuxk586OnD/Oxu7fxNz99gX9689lTPtL0ucOnOdE3wnVrSmMGqHR4nV3TbEfwiEgV8CpgtzfHyrKe8RbNVGJNHBKdEpE3GGYCb12/kA9fs5wfP3OEf/3t/ik//qbt7QT9Pq5e1TLlx84HryP5ucB/2b68D/iRqv7SiwOllO20E4hMvchbE4dYKZReVaA0GGYaH7l2OUdOD/Evv9nL/KYq3nzB1NzFqiqbdrRzyZmzqStin0AueCryqroNOM/LYzikulVzZ9ekGgA1lVQH/QyMRhkKx+JFxAwGw+QQEb7wxnW09w7xiZ9sY25DJZec6X0n6M7jvRw+NcSfX3Wm58eaLGWTfpFKtzVNJF8Mu6Y6GGBoNMLwqLFrDIZCEgz4+I93XsCy5lo+8L1n2N3e6/kxN+/owCdw7Vml7cdDGYl8Kty9uIkFyqa8KdSE/GOevMcplAbDTKO+soI7b7mQ6pCfW+58inaP00A3b2/nwiWzmF1b+mNRykbks+t2se2aQNyT9zqF0mCYicxrrOKOmy+kdyjMLd99in57XuVCc/BEP3s6+kq2Vk0yZaM2qe2a7FUop4qakOPJR0s6p9ZgmM6smdfAv7/zAvZ29PGnP3jWk1o8m3d0AJT0KFc3ZSPyqWL5RLumuJF8VUWAwRErkjeevMHgHVesaOZzb1jLw3tP8Kl7ticEe4Vg8452zl7QMG1qQ5WNyGfteHUtL1BJmrxwIvlwVE0kbzB4zNtesYgPXn0mP3zqMN94oHA59Md7hnj+cPe0ieJhCke8FoN0kby/SJ68g4nkDQbv+ctXreDI6SG+dJ+VQ/8H500+h/4+26qZLn48lJHIpx4MldqTL4Zd45QbBjwvUGYwGKwc+i++6Wzae4b52N3baK2v5JXLJpdDv2l7O8tbalnWXFugVnpPGdk1uQt3MaboMpG8wTD1BAM+vvmuC1gyu4Y//t4z7O3om/C+Tg2M8uShU9PKqoFyEvkUyxLsGpewF2cwlCuSNymUBsOU0VBl5dBXVlg59J29E8uh/82uDqIxnVZWDZSTyOfT8VqEwVBukTeRvMEwtSxoqubOmy/k9OAot3z3KQYmkEO/eXs78xurWDOv3oMWekf5iHzKFMrSqV1TEzJ2jcFQTNbOb+Abbz+f3e19/Nn/PEskjxz6/pEIv993kuvXtk15SePJUjYin4rSql0zJuymCqXBUByuWtXCZ1+/lgf3nOBTP9+Rcw79g3s6GY3Gpp0fD+WUXZO1nvwYRYnkTcerwVASvP2iRRw5Pci/P3iAhbOq+NMrs1eS3LS9nTm1QS5Y3DQFLSwsZSPyqUiM5N12zdS3JcGTNymUBkNR+evrVnLk9BD/tGkP8xureP2589NuOxyO8sDuTl537vyiuACTpWxEPmskb6/3SX7ploWi2njyBkPJ4PMJ//yWs+noHeajP7Zy6DcsnZ1y28cOnGRgNMrGEp/mLx1l48mn7nh1r7cohlUDicJuUigNhuITCvi5/V3rWTS7mlv/+2n2d6bOod+0vZ26UGDSA6mKRVmrjbtTxRH3YgyEAquz1xF6U7vGYCgNGqoruPPmCwkG/Lznjqfo7EvMoY9EY9y/s4NrzmohGJiecjk9W52CXO2aYtStcagJ+RGB0DT9ZzEYypGFs6q54+b1nBoY5b3ffTohh/7JQ6c4PRiedgOg3JSN2mQXeTuSL2K/SVXQT2XAP+3ybA2GcufsBY18/e3nseNYDx+667l4Dv19OzoIBXxcvqK5yC2cOJ6KvIgsFJEHRGSniOwQkQ97dqxUnnyKPPli2TVgpVGazBqDoTS55qxWPvO6Nfx2dye3/d8OYjFl0/Z2rljRnFB7arrhdcsjwF+p6rMiUgc8IyL3q+rOQh8oZVkDV9ers7qYKVDVQb/JrDEYSph3XbyEI91DfOuhg/QPR2jvHeZja1cWu1mTwlORV9XjwHH7eZ+I7ALmAwUX+dTHH3se73gtqicfoLIiXLTjGwyG7Hx84yqOnh7inuePEfAJ16yanqmTDlN2DyIiS4DzgCeSlt8K3AqwaNGiie/f9fy8RY0893I3t16+1HUc628xRf7sBQ3MmQazuxsMMxmfT/jSW85hYCTCrJoQDdUVxW7SpJgSkReRWuAnwEdUtde9TlVvB24HWL9+/YQnY3S0u6rCz8/+9JJx6x1x9xexq/mjG1cV7+AGgyFnKiv83HnLKwo+P2wx8FzyRKQCS+B/oKo/9fBI9vEyb1XMFEqDwTC9KIdMOK+zawT4DrBLVb/i7bFyW18OF81gMBhyxetI/hLgXcDVIvK8/bjRiwNlK1swZtcYkTcYDDMHr7NrHiHNHNteke5gpZBCaTAYDFNNGY14dfyY1OudSN64NQaDYSZRPiKfbX0J1K4xGAyGqaZ8RD5zIB+P9I1dYzAYZhLlI/JkLyUsRZowxGAwGIpF2Yi8QyYJF4o7GMpgMBimmrKRvFwCdJ+I8eQNBsOMomxE3iGTHWPsGoPBMNMoG5HP1vFqrRPT8WowGGYUZSTykvA39TYmhdJgMMwsykbkHTJpuGXXTF1bDAaDodiUjcjnot3GrjEYDDON8hH5HDx5n5jBUAaDYWZRNiLvkNmuEZNdYzAYZhTlJ/IZYnmr43UKG2MwGAxFpmxE3pmlK2Mkj7FrDAbDzKJsRD5mq3zGPHlj1xgMhhlG2Yj8WCSfXsR9Jk/eYDDMMMpG5HNBxKRQGgyGmUXZiXymQN1nBkMZDIYZhqciLyJ3iEiniGz38jiQW8crZjCUwWCYYXgdyX8XuN7jYwDujldTu8ZgMBgcPBV5VX0YOOXlMeLHsv9mt2uMyBsMhplD2XjyDVUVAFyxojntNk3VQWbVVExVkwwGg6HoBIrdABG5FbgVYNGiRRPez6yaII98/Cpa6yvTbvO9915EddA/4WMYDAbDdKPokbyq3q6q61V1fXNz+ig8FxY0VVORYRLX5roQNaGi/64ZDAbDlFF0kTcYDAaDd3idQnkX8DiwUkSOiMh7vTyewWAwGBLx1LtQ1Zu83L/BYDAYMmPsGoPBYChjjMgbDAZDGWNE3mAwGMoYI/IGg8FQxog6lb1KABE5AbyU59vmACc9aE4pMxPPGWbmec/Ec4aZed6TOefFqppyoFFJifxEEJGnVXV9sdsxlczEc4aZed4z8ZxhZp63V+ds7BqDwWAoY4zIGwwGQxlTDiJ/e7EbUARm4jnDzDzvmXjOMDPP25NznvaevMFgMBjSUw6RvMFgMBjSYETeYDAYyphpK/Iicr2I7BGR/SLyiWK3p1CIyEIReUBEdorIDhH5sL18lojcLyL77L9N9nIRkX+1P4dtInJ+cc9gcoiIX0SeE5Ff2q/PEJEn7PP7XxEJ2stD9uv99volRW34BBGRRhG5W0R2i8guEbl4JlxrEfkL+/97u4jcJSKV5XitReQOEekUke2uZXlfXxF5j739PhF5Tz5tmJYiLyJ+4BvADcBq4CYRWV3cVhWMCPBXqroa2AD8mX1unwB+q6rLgd/ar8H6DJbbj1uB/5j6JheUDwO7XK+/CPyLqp4JnAacctXvBU7by//F3m468jVgk6quAs7BOveyvtYiMh/4ELBeVdcCfuBtlOe1/i5wfdKyvK6viMwCPg1cBLwC+LTzw5ATqjrtHsDFwGbX608Cnyx2uzw6158DrwL2AHPtZXOBPfbzbwE3ubaPbzfdHsAC+5/+auCXgGCNAAwkX3dgM3Cx/TxgbyfFPoc8z7cBeDG53eV+rYH5wGFgln3tfglsLNdrDSwBtk/0+gI3Ad9yLU/YLttjWkbyjP2TOByxl5UV9m3pecATQKuqHrdXtQOt9vNy+iy+CnwMiNmvZwPdqhqxX7vPLX7e9voee/vpxBnACeBO26L6TxGpocyvtaoeBb4EvAwcx7p2z1De19pNvtd3Utd9uop82SMitcBPgI+oaq97nVo/52WV+yoirwE6VfWZYrdlCgkA5wP/oarnAQOM3boDZXutm4DXY/3IzQNqGG9pzAim4vpOV5E/Cix0vV5gLysLRKQCS+B/oKo/tRd3iMhce/1coNNeXi6fxSXA60TkEPBDLMvma0CjiDgzmLnPLX7e9voGoGsqG1wAjgBHVPUJ+/XdWKJf7tf6WuBFVT2hqmHgp1jXv5yvtZt8r++krvt0FfmngOV2b3wQq9PmF0VuU0EQEQG+A+xS1a+4Vv0CcHrV34Pl1TvL3233zG8Aely3gtMGVf2kqi5Q1SVY1/N3qvoO4AHgzfZmyeftfB5vtrefVhGvqrYDh0Vkpb3oGmAnZX6tsWyaDSJSbf+/O+ddttc6iXyv72bgOhFpsu+CrrOX5UaxOyUm0ZlxI7AXOAD8bbHbU8DzuhTr9m0b8Lz9uBHLg/wtsA/4DTDL3l6wMo0OAC9gZSwU/Twm+RlcCfzSfr4UeBLYD/wYCNnLK+3X++31S4vd7gme67nA0/b1vgdomgnXGvgMsBvYDnwPCJXjtQbuwup3CGPdub13ItcX+CP7/PcDt+TTBlPWwGAwGMqY6WrXGAwGgyEHjMgbDAZDGWNE3mAwGMoYI/IGg8FQxhiRN5QFInKziMwrdjsmgohcKSKvLHY7DOWJEXlDuXAz1uhJT3AN0vGCK4G8RN7j9hjKCJNCaShZ7DouP8Ia4ecHPouVJ/wVoBarUNXNWKMlv4s1CnAIq5jVUIr9HbL3d4O93dtVdb+IvBb4f0AQayTlO1S1Q0RuA5Zh5W+/jFUI73tYw/AB/lxVHxORK7HyvruBdfYxXsCqqFkFvEFVD4hIM/BNYJH9/o/Ybd4CRLHq2HwQK388YTtVfTS5Pap6Ux4fp2GmUuzBAuZhHukewJuAb7teNwCPAc326z8E7rCfP0iWwUHAIeyBc8C7GRtw1cRYwPM+4Mv289uwCmdV2a+rgUr7+XLgafv5lVgCPxdrUM9R4DP2ug8DX7Wf/w9wqf18EdaoZuc4f+1qZ6bt4u0xD/PI5WFu+QylzAvAl0Xki1jlaE8Da4H7rdHw+LFGE+bDXa6//2I/XwD8r11HJIhV/tfhFzp2V1ABfF1EzsWKvFe4tntK7RIDInIAuM91DlfZz68FVtttB6i3C9Elk2k7d3sMhqwYkTeULKq6154d50bgH4DfATtU9eLJ7DbF838DvqKqv7Ctl9tc2wy4nv8F0IE1uYcPGHatG3E9j7lexxj7nvmADarqfh8uMSeH7QaSNzYYMmE6Xg0li50tM6iq3wf+GWtmnGYRudheXyEia+zN+4C6HHb7h66/j9vPGxir6pdparUG4LiqxoB3Yd1J5MN9WJ47APYdAYxve7rtDIa8MSJvKGXWAU+KyPNY05/9HVYVwi+KyFas4m1OVsp3gW+KyPMiUpVhn00isg3LK/8Le9ltwI9F5Bmsztx0/DvwHvvYq8g/qv4QsN6ev3Mn8AF7+f8Bf2C3/bIM2xkMeWOyawwzBju7Zr2qZhJyg6GsMJG8wWAwlDGm49VQdojIz7CmlnPzcbUmJDEYZhTGrjEYDIYyxtg1BoPBUMYYkTcYDIYyxoi8wWAwlDFG5A0Gg6GMMSJvMBgMZYwReYPBYChj/j+v8vwFW1re/wAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "from qcodes.dataset.plotting import plot_by_id\n", + "plot_by_id(msmt.dataset.run_id);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As can be seen, the measurement code did not increase much in complexity even though we started performing more complex measurements. this measurement cannot be performed using `dond`, though it can still be performed by `Measurement`.\n", + "\n", + "One surprising fact from the above measurement is that we didn't need to define new parameters for the inner sweep and for the measurement recording how many times `random_parameter` is above half. This is a feature of the `MeasurementLoop`: parameters aren't needed for sweeps and measurements! This can significantly simplify creating complex measurements.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Nested measurements" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One big feature of the `MeasurementLoop` is that one can nest measurements. This is largely because we don't have to define our parameters beforehand. Here we show an example:" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 20. \n" + ] + } + ], + "source": [ + "with MeasurementLoop('outer_measurement') as outer_msmt:\n", + " for set_val1 in Sweep(range(10), 'outer_sweep'):\n", + "\n", + " with MeasurementLoop('inner_measurement') as inner_msmt:\n", + " for set_val2 in Sweep(range(10), 'inner_sweep'):\n", + " inner_msmt.measure(random_parameter)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "from qcodes.dataset.plotting import plot_by_id\n", + "plot_by_id(outer_msmt.dataset.run_id);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When we instantiate the inner measurement, it sees that another measurement is already running, and so it realizes that it is part of this larger measurement and so attaches to it. It will therefore use the dimensionality of the outer measurement.\n", + "\n", + "You can again ask yourself why this is useful. One big reason is that this allows us to functionalize measurements. For example, we can create a function `retune_device()` that performs a complex retuning sequence, in this case finding the minimum of a 2D quadratic:" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "metadata": {}, + "outputs": [], + "source": [ + "from scipy import optimize\n", + "\n", + "\n", + "def retune_device():\n", + " with MeasurementLoop('retune_device') as msmt:\n", + " # Create a random minimal point\n", + " x0 = msmt.measure(2 * (np.random.rand() - 0.5), 'x0')\n", + " y0 = msmt.measure(2 * (np.random.rand() - 0.5), 'y0')\n", + " print(f'{x0=:.3f}, {y0=:.3f}', end=', \\t')\n", + "\n", + " minimization_function = lambda x: (x[0] - x0)**2 + (x[1] - y0)**2\n", + "\n", + " intermediary_results = []\n", + " max_iter = 100\n", + " optimize.minimize(\n", + " minimization_function, \n", + " x0=(100*np.random.rand(), 100*np.random.rand()),\n", + " options={'maxiter': max_iter},\n", + " callback=intermediary_results.append\n", + " )\n", + "\n", + " for k in Sweep(range(max_iter), 'iteration'):\n", + " if k >= len(intermediary_results):\n", + " msmt.step_out() # See section \"Fixed measurement order\"\n", + " break\n", + "\n", + " msmt.measure(intermediary_results[k][0], 'x')\n", + " msmt.measure(intermediary_results[k][1], 'y')\n", + " msmt.measure(x0 - intermediary_results[k][0], 'x_error')\n", + " msmt.measure(y0 - intermediary_results[k][1], 'y_error')\n", + " \n", + " print(\n", + " f'x_error = {x0 - intermediary_results[-1][0]:.4g}, '\n", + " f'y_error = {y0 - intermediary_results[-1][1]:.4g}')\n", + " return msmt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we perform the retuning sequence and plot the results:" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 31. \n", + "x0=-0.676, y0=0.351, \tx_error = -6.712e-08, y_error = -6.954e-08\n" + ] + } + ], + "source": [ + "msmt = retune_device()" + ] + }, + { + "cell_type": "code", + "execution_count": 100, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "dataset = msmt.dataset.to_xarray_dataarray_dict()\n", + "from matplotlib import pyplot as plt\n", + "fig, axes = plt.subplots(1, 2, figsize=(10,4))\n", + "ax = axes[0]\n", + "ax.plot(dataset['x0'], dataset['y0'], '*', ms=20, label='Target', color='C8')\n", + "for k, (x, y) in enumerate(zip(dataset['x'], dataset['y']), start=1):\n", + " ax.plot(x, y, 'o', label=f'Guess {k}')\n", + "ax.legend()\n", + "ax.set_xlabel('X')\n", + "ax.set_ylabel('Y')\n", + "ax.grid('on')\n", + "\n", + "ax = axes[1]\n", + "for k, (x, y) in enumerate(zip(dataset['x_error'], dataset['y_error']), start=1):\n", + " ax.plot(x, y, 'o', label=f'Guess {k}', color=f'C{k}')\n", + "ax.legend()\n", + "ax.set_xlabel('X error')\n", + "ax.set_ylabel('Y error')\n", + "ax.grid('on')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, since we can nest measurements, we can simply incorporate it as a function in another measurement:" + ] + }, + { + "cell_type": "code", + "execution_count": 101, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 32. \n", + "x0=0.606, y0=-0.378, \tx_error = 1.036e-07, y_error = 1.664e-06\n", + "x0=-0.908, y0=-0.610, \tx_error = -2.148e-08, y_error = -4.82e-09\n", + "x0=-0.821, y0=0.242, \tx_error = -1.613e-06, y_error = 2.737e-06\n", + "x0=-0.090, y0=0.307, \tx_error = 2.077e-06, y_error = -4.658e-06\n", + "x0=-0.927, y0=0.470, \tx_error = 1.126e-08, y_error = 7.831e-08\n", + "x0=0.364, y0=-0.038, \tx_error = -1.531e-07, y_error = -2.273e-07\n", + "x0=-0.878, y0=0.987, \tx_error = 1.254e-06, y_error = 5.796e-07\n", + "x0=0.754, y0=-0.254, \tx_error = 2.951e-07, y_error = 3.297e-07\n", + "x0=0.855, y0=0.030, \tx_error = 2.321e-07, y_error = 1.216e-06\n", + "x0=0.198, y0=-0.337, \tx_error = 8.037e-08, y_error = 7.971e-08\n", + "x0=0.445, y0=0.292, \tx_error = 4.155e-09, y_error = 4.147e-09\n" + ] + } + ], + "source": [ + "with MeasurementLoop('measurement_with_retuning') as msmt:\n", + " for k in range(11):\n", + " result = retune_device()\n", + "\n", + " msmt.measure(random_parameter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ability to turn a measurement into a function that can then be used within other measurements allows the experimentalist to modularize measurements. This can help once measurements become more complex" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fixed measurement order of parameters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One of the main reasons why the `MeasurementLoop` is able to make so much of the code implicit is because it assumes a fixed order in which parameters are swept / measured. This needs to be adhered to, or things can break. This restriction can be illustrated with the following example.\n", + "\n", + "For this example we first use the `Measurement`. We sweep a parameter from 0 to 10 in integer steps. If the integer is odd, we measure `random_parameter` which returns a random value. However, if it's even, we measure `fixed_parameter` which always returns 42.\n", + "\n", + "Importantly, the first parameter that is being measured in every sweep iteration changes between `random_parameter` and `fixed_parameter`" + ] + }, + { + "cell_type": "code", + "execution_count": 102, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 33. \n" + ] + } + ], + "source": [ + "from qcodes.dataset.measurements import Measurement\n", + "context_meas = Measurement(name='varied_parameter_order_measurement')\n", + "\n", + "# Register the independent parameter...\n", + "context_meas.register_parameter(set_parameter)\n", + "# ...then register the dependent parameter\n", + "context_meas.register_parameter(random_parameter, setpoints=(set_parameter,))\n", + "\n", + "# We also add a second parameter that always returns 42\n", + "fixed_parameter = Parameter('fixed_parameter', get_cmd = lambda: 42)\n", + "context_meas.register_parameter(fixed_parameter, setpoints=(set_parameter,))\n", + "\n", + "with context_meas.run() as datasaver:\n", + " for set_v in np.linspace(0, 10, 11):\n", + " set_parameter(set_v)\n", + "\n", + " if set_v % 2:\n", + " get_v = random_parameter()\n", + " datasaver.add_result((set_parameter, set_v),\n", + " (random_parameter, get_v))\n", + " else:\n", + " get_v = fixed_parameter()\n", + " datasaver.add_result((set_parameter, set_v),\n", + " (fixed_parameter, get_v))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We see that both parameters are measured perfectly fine" + ] + }, + { + "cell_type": "code", + "execution_count": 105, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYIAAAEXCAYAAACgUUN5AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAAA0WklEQVR4nO3dd5wV1fnH8c+XpXeQKixNaSsKyNJijYolGomaKIgFLGii0ejPJJqYaEzyU/NLTEw0UUFAQECNJdixG5UFliLSBWTpvfctz++PmdXLuix74V5md+/zfr3ua6ecmXlu2fvcOefMGZkZzjnnUlelqANwzjkXLU8EzjmX4jwROOdcivNE4JxzKc4TgXPOpThPBM45l+I8EbiUIOlXkoZHHUdZIOk8Sa9EHUfUJN0vaWwpy74o6YJkxxQVTwRHmaRlkvZI2ilpraRRkmofheOuklRD0lmSXiqy7gNJGyRtl/S5pP4x674r6QtJWyVtkvSypBalPGYbSRY+19jHFYl+fodiZv9rZjcc7eNKGizpk0OU+VDSEccm6UxJK0tR9I/AQzHbmaTjE3D8Un+xlkMPA3+IOohk8UQQje+bWW2gG9AduCeZB5OUDmwysz1AD2BGkSK3A83NrC4wFBgrqXm4bh5wnpnVB44FvgT+FWcI9c2sdszjucN9LodDUuWjebyyTFJPoJ6ZZUUdS3liZlOBupIyo44lGTwRRMjM1gJvEySEYn/RhWcQ54TT90t6XtJoSTskzS3lBzMTmB4zfUAiMLPZZpZXOAtUAdLDdevMbHVM8XwgEb8eq0qaJemn4XyapE8l/Tacv1/SvyU9Fz7XGZK6xmx/bHi6vkHSV5Jui1lXuO1YSduBwbG/VmPOVIZIWiFpi6SbJfWUNDs8+3msSLzXSZofln1bUuuYdRZu/2W47eMKdAaeAPqGZ0Jbi3kd/gicBjwWlnksXN5J0juSNktaKOnymG2+J2le+LqsknSXpFrAm8CxMWdexxbz0l8AfBSzr4/Dyc9jz9YkXRS+P1slfSbppJhtfhked0cY29mSzgd+BVwR7ufzEt7+wjOlpeE+vpI0KFx+nKT3w7PPjZKelVQ/Zrtlkn4evk+7JD0tqamkN8N9vSupQZH3eaik1ZLWSLqrhJj6hM91q4Iz4zOLFPkQuLCk51VumZk/juIDWAacE063BL4AHg3nzwRWllD+fmAv8D0gDXgQyCrhWPcBW8NtdofT+cC2cDotpuxrYTkD3gIqxaxrFZYvAHKBwaV8rm3C/VU+yPouwBagM/BrIKswpvC55gI/JEhMdwFfhdOVCBLbb4GqQDtgKcGZS+y2PwjL1giXjS0S1xNAdeDc8Lm/AjQBWgDrgTPC8v2BxWGclYF7gc9inoeFr1/98LXaAJwfrhsMfHKI1+lD4IaY+VrACmBIeLzuwEYgI1y/BjgtnG4AnHywz08xx3oB+HmRZQYcHzPfPXz+vQk+Z9cSfA6rAR3D2I6NeS2Pi3ndx5bic1EL2A50DOebAyeE08cD/cJjNQY+Bv5W5P8hC2ga8z7NCGOuDrwP3FfkfR4fHvPE8L05p2i84b42EfxvVQpj2AQ0jjn2ncBLUX+HJOPhZwTReEXSDoJ/qPUEX9il9YmZvWFm+cAYoOvBCprZ74BGBF+gbQk+5G+ZWT0zqx/uo7DsRUCdsMwkMyuIWbfcgqqhRgRfggviiBdgY/grq/DROdzvHIJ611cIvuivjo0JmG5m/zazXOARgn/0PkBPgn/QB8xsv5ktBYYBA2K2nWxmr5hZgQVVYsX5vZntNbNJwC5gvJmtN7NVwH8JvlwAbgYeNLP5Fpw5/S/QLfasAHjIzLaa2XLgA8KzvMN0EbDMzEaaWZ6ZzQReBH4Urs8FMiTVNbMtZla0qq8k9YEdhygzFHjSzKaYWb6ZPQPsI3jt8wm+pDMkVTGzZWa2JI7jFyoAukiqYWZrzGwugJktNrN3zGyfmW0geN/PKLLtPyw4Uy18n6aY2Uwz2wu8zDfvW6HfmdkuM/sCGAkMLCaeq4A3wv+tAjN7B8gm+H8otIPg9atwPBFE4wdmVofgF1wngi/Y0lobM70bqK5i6sAldQurIrYQ/MpaSPAFdWb4ZXxp0W3MLNfM3gTOlXRxMes3A88A/ynumCVoFCaewsf8mHXPAK0J/gm/LLLdiphjFwArCdopWhNUgXydXAiqJZoWt20J1sVM7ylmvrARvzXwaMyxNgMi+BVZqOj7ciQdAFoDvYs8v0FAs3D9ZQRfUDmSPpLUN459byFI+Ic6/v8UOX46wVnAYuBnBL+m10uacJAqqIMys13AFQQJdo2k1yV1AgireSaEVU/bgbF8+/+jtO9bodjPQg7BZ6io1sCPijznUwnOVgrVITgzrnA8EUTIzD4CRgF/DhftAmoWrpeURnB6fDj7nhX+iv8j8Ntweh7QNfwyfqmEzSsDx5WwrglQ93DiKsY/CapVzpN0apF16YUTkioRVKWtJvjH/qpIcqljZrG/3hI5rO4K4KYix6thZp+VYtvSxFG0zArgoyLHq21mPwYws2lm1p/gfXgFeD6OY80GOhyizArgj0WOX9PMxofHH2dmpxJ8eRpBj5rSHp9wH2+bWT+CL9oFBGd0EJxtGXCiBZ0XriJIukciPWa6FcFnqKgVwJgiz7mWmT0UU6YzUGLbR3nliSB6fwP6KWgIXUTwC/9CSVUIqmGqHeH+ewAzJFXlm190XwsbJS9Q0LW0iqSrgNMJGxQlXSqpo6RKkhoTnKrPDM8OChtmPzycwCRdHcY3GLgNeEYHdqXtER6/MsGv0H0E9cNTgR1ho2UNBQ3NXRT0iEmGJ4B7JJ0Qxl1P0o8OsU2hdUDL8PUvqUy7mPnXgA6Srg7fkyoKGrI7K2hkHySpXlhltp2gmqVwP8dIqlfCsd7g21UtRY8/DLhZUm8FaoWfyTrhZ+EsSdUI2lX2FDl+mzBpH1T4q7+/ggbufcDOmH3UCee3Keim/POS9lVKv5FUM3z/hgDF9VobC3xfwTUWaZKqK+i80TKmzBkEDfIVjieCiIX1oKMJfrVvA34CDAdWEZwhlKZfeEkKu4ueCMwpZr0IT/MJGtJuB66IqXduQdB4vIOgYbsAuCRm+3Tg00PEsFUHXkdwp6RWBEnwGjPbaWbjCOpk/xqz3X8IqhC2AFcDl4bVV/kE9ejdCNo/NhK8ZiV9AR42M3uZ4FfvhLC6Yg5B75vSeB+YC6yVtPEgZR4FfqigR9LfzWwHQQP2AIJfr2vD4xf+KLgaWBbGcjNBtRFmtoCgYXRpWL3xrSqQ8H3dJql3zOL7CZLwVkmXm1k2cCPwGMFrv5ggWRPG8BDBa76W4KyksPvzC+HfTZJKareoRNDwupqgmu0M4Mfhut8BJxN0aHgdKOnMtbQ+Cp/De8CfwzahA5jZCoJOAb8i+D9YQZCEKsHX3W53WtCNtMKRmd+Yxh0+SbOAs81sU4L3ez9BT5arErlfB5LOBX5iZj+IOpZkktSGsKeZfdM9+nD39SLwtJm9kYjYyhq/0MYdETPrFnUMLj7hL+Jv/Sp2B2dml0UdQzJ5InDOJYWknQdZdYGZ/feoBuNK5FVDzjmX4ryx2DnnUpwnAuecS3Hlso2gUaNG1qZNm6jDcM65cmX69OkbzexbF6mWy0TQpk0bsrOzow7DOefKFUk5xS33qiHnnEtxngiccy7FeSJwzrkUl/REIOl8BXcxWizp7mLWt5b0noI7Dn1YZJAn55xzSZbURBAOo/w4wQBdGcBASRlFiv0ZGG1mJwEPENx1yznn3FGS7DOCXsBiM1tqZvuBCQQj/MXKIBihEYIbpxRd75xzLomSnQhacODdgVZy4F2dILjRQ+Hdsi4B6kg6JslxOedcubM3N59kDAtUFhqL7wLOkDSTYFzyVQT3RT2ApKGSsiVlb9iw4WjH6JxzkVmxeTcPvjmfvg++R3bOloTvP9kXlK3iwNvEtQyXfc3MVhOeEYR3p7rMzLYW3ZGZPQU8BZCZmekj5TnnKrT8AuPjRRsYPXkZHy7aQCWJfp2bUrta4r+2k50IpgHtJbUlSAADgCtjC0hqBGwOb05+DzAiyTE551yZtXnXfl7IXsHYKTms2LyHxnWq8dOz2jOwVzrN69VIyjGTmgjMLE/SrcDbQBowwszmSnoAyDazicCZwIOSDPgYuCWZMTnnXFk0a8VWRk9exmuz17A/r4BebRvyy/M7cW5GM6pWTm4tfrm8H0FmZqb5WEPOufJub24+Ez9fzdisHGav3EatqmlccnILru7Tho7N6iT8eJKmm1lm0eXlctA555wrz5Zt3MWzU3J4Pnsl2/bk0r5JbR7ofwKXdG9BnepVjno8ngicc+4oyC8wPliwntFZOXy8aAOVK4nzTmjG1X1b07ttQyRFFpsnAuecS6JNO/fxXPYKns1azqqte2hatxp3nNOBAb3SaVq3etThAZ4InHMu4cyMGcu3MjYrh9dnr2F/fgF92x3DvRd25pyMplRJKwuXcH3DE4FzziXI7v15TJy1mjFZOcxdvZ3a1SozsFc6V/VpTfumiW/8TRRPBM45d4SWbtjJ2KzlvDB9BTv25tGpWR3+8IMuXNK9BbWScAFYopX9CJ1zrgzKyy/gvQXrGZuVw3+/3EiVNHF+l+Zc07c1ma0bRNr4Gy9PBM45F4cNO/bx3LTljJuynNXb9tK8XnX+p18HruiVTpM6ZaPxN16eCJxz7hDMjOk5Wxg9OYc356whN9849fhG/Pb7J3BO5yZULmONv/HyROCccwexa18er8xaxZjJOSxYu4M61StzVZ/WXNWnNcc1rh11eAnjicA554pYvH4nY7NyeHH6Snbsy6Nz87o8eOmJ9O92LDWrVryvzYr3jJxz7jDk5Rfwzrx1jMnK4bMlm6iaVonvnRhc+Xtyq/LV+BsvTwTOuZS2fvtexk9dwfipy1m7fS8t6tfg5+d15Iqe6TSqXS3q8I4KTwTOuZRjZkz9ajOjs3J4e85a8gqM0zs05vc/6MJZnZqQVqni/vovjicC51zK2Lkvj5dnrmLs5BwWrttB3eqVGfydNgzq05q2jWpFHV5kPBE45yq8Ret2MGZyDi/NWMmu/fl0aVGXP112Et/veiw1qqZFHV7kPBE45yqk3PwCJs1dx5isZWQt3UzVypW46KTmXN2nNd3S61foxt94eSJwzlUoa7ftZfzU5Yyfupz1O/bRskEN7r6gE5dnptOwVtWowyuTPBE458o9M2Py0k2Mzcrh7bnrKDDjjA6Neahva87okHqNv/HyROCcK7d27M3lpRmrGJOVw+L1O6lfswrXn9qWQb1b0fqY1G38jZcnAudcubNg7XbGTM7h5Zmr2L0/n64t6/HnH3XlopOaU72KN/7GK+mJQNL5wKNAGjDczB4qsr4V8AxQPyxzt5m9key4nHPly/68At6au5axk3OYumwz1SpX4uKux3JVn9Z0Ta8fdXjlWlITgaQ04HGgH7ASmCZpopnNiyl2L/C8mf1LUgbwBtAmmXE558qP1Vv3hI2/K9i4cx+tGtbkV9/rxI96pNPAG38TItlnBL2AxWa2FEDSBKA/EJsIDKgbTtcDVic5JudcGWdmfLp4E2OylvHu/PUUmHFWxyZc3bc1p7dvTCVv/E2oZCeCFsCKmPmVQO8iZe4HJkn6KVALOKe4HUkaCgwFaNWqVcIDdc5Fb9ueXF6cvpKxU3JYumEXDWtV5cbT2jGodyvSG9aMOrwKqyw0Fg8ERpnZXyT1BcZI6mJmBbGFzOwp4CmAzMxMiyBO51ySzFu9nTFZy3hl5mr25ObTvVV9/npFVy7o4o2/R0OyE8EqID1mvmW4LNb1wPkAZjZZUnWgEbA+ybE55yJUUGC8Ons1oyfnMD1nC9WrVKJ/1xZc3bc1XVrUizq8lJLsRDANaC+pLUECGABcWaTMcuBsYJSkzkB1YEOS43LORSi/wPjli7P59/SVtDmmJvde2Jkf9UinXs0qUYeWkpKaCMwsT9KtwNsEXUNHmNlcSQ8A2WY2EfgfYJikOwgajgebmVf9OFdB5RcYP3/hc16auYrbzm7Pz85u742/EUt6G0F4TcAbRZb9NmZ6HnBKsuNwzkUvL7+A/3nhc/4zazV39uvAbWe3jzokR9loLHbOpYC8/ALueP5zXv18NT8/ryO3fPf4qENyIU8Ezrmky80v4GcTZvH6F2u4+4JO3HzGcVGH5GJ4InDOJdX+vAJuGz+Tt+au5dff68yNp7eLOiRXhCcC51zS7M8r4JZxM3hn3jp+c1EG15/aNuqQXDE8ETjnkmJfXj63PDuDd+ev5/7vZzD4FE8CZZUnAudcwu3Nzecnz87g/QXr+X3/E7i6b5uoQ3Il8ETgnEuovbn53DRmOh8t2sAfL+nCoN6tow7JHYInAudcwuzNzefG0dl8sngjD116IgN6+QCR5YEnAudcQuzZn88No6fx2ZJNPHzZSVyemX7ojVyZ4InAOXfEdu/P4/pR2WR9tYk//7Arl/VoGXVILg6eCJxzR2TXvjyGjJpG9rLN/PXybvyge4uoQ3Jx8kTgnDtsO/flMWTkVKbnbOGvV3SjfzdPAuWRJwLn3GHZsTeXwSOnMWvFVv4+sDsXnXRs1CG5w+SJwDkXt+17c7l2xFS+WLmNxwZ254ITm0cdkjsCngicc3HZtieXa0ZMZe6qbTx25cmc36VZ1CG5I+SJwDlXatt253L1iCnMX7Odf13Vg34ZTaMOySWAJwLnXKls2bWfq56ewpfrdvLEVT04u7MngYrCE4Fz7pA279rPoOFTWLJhJ09e04PvdmwSdUgugSqVppACfpmgcylo0859XDksi6UbdjLsmkxPAhVQqRJBeDP5Nw5Z0DlXoWzYsY+Bw7L4auMunr62J2d0aBx1SC4JSpUIQjMk9Yz3AJLOl7RQ0mJJdxez/q+SZoWPRZK2xnsM51zird+xl4HDsli+eTcjB/fk1PaNog7JJUk8bQS9gUGScoBdgAhOFk462AaS0oDHgX7ASmCapIlmNq+wjJndEVP+p0D3+J6Ccy7R1m8PksDqrXsZNaQXfdodE3VILoniSQTnHcb+ewGLzWwpgKQJQH9g3kHKDwTuO4zjOOcSZO22vVw5LIu12/fyzHW96NW2YdQhuSQrddWQmeUA6cBZ4fTuUmzfAlgRM78yXPYtkloDbYH3SxuTcy6x1mzbw4CnJrNu+15GexJIGaVOBJLuA34J3BMuqgKMTWAsA4B/m1n+QY4/VFK2pOwNGzYk8LDOOYBVW/dwxZNZbNy5n9HX9yazjSeBVBFPY/ElwMUE7QOY2WqgziG2WUVwFlGoZbisOAOA8QfbkZk9ZWaZZpbZuLH3XHAukVZu2c2ApyazZdd+xlzfix6tG0QdkjuK4kkE+8NupAYgqVYptpkGtJfUVlJVgi/7iUULSeoENAAmxxGPcy4BVmzezRVPZrFtdy5jb+hN91aeBFJNPIngeUlPAvUl3Qi8CwwvaQMzywNuBd4G5gPPm9lcSQ9Iujim6ABgQphonHNHSc6mXVzx5GR27stj3I196JpeP+qQXAQUz3evpH7AuQRdR982s3eSFVhJMjMzLTs7O4pDO1dhLNu4i4HDstiTm8/Y63vTpUW9qENySSZpupllFl1e6u6jkh42s18C7xSzzDlXjizdsJOBw7LYn1fAuBv6kHFs3ahDchGKp2qoXzHLLkhUIM65o2Px+p1c8VQWefnG+KGeBFwpzggk/Rj4CdBO0uyYVXWAT5MVmHMu8b5ct4OBw6YAQRLo0PRQHf9cKihN1dA44E3gQSB2rKAdZrY5KVE55xJu4dodDBqehSTG39iH45t4EnCBQ1YNmdk2M1tmZgM58MriSpLaJj1C59wRm79mOwOHZVFJYsJQTwLuQEdyZXFVEntlsXMuCeau3saVw7KomlaJ527qy3GNa0cdkitjkn1lsXMuQnNWbWPQ8ClUr5LGhKF9aNuoNNeBulST7CuLnXMRmb1yK1cOy6JW1co8N7QvbTwJuIM40iuLhyUnLOfckZi1YiuDhk+hbo0qTBjah1bH1Iw6JFeGlfqCMjP7c3hl8XagI/DbqK4sds4d3IzlW7j26anUr1WF8Tf2oWUDTwKuZPHcmAYze0fSlMLtJDX0LqTOlR3TczZz7YhpHFO7KuNv7MOx9WtEHZIrB+IZYuIm4HfAXqCA8FaVQLvkhOaci8e0ZZsZPGIqTepWZ9yNvWlez5OAK514zgjuArqY2cZkBeOcOzxTlm5iyKhpNKtXnfE39qFp3epRh+TKkXgai5cQ3J7SOVeGfLZkI4NHTqN5vepM8CTgDkM8ZwT3AJ+FbQT7Chea2W0Jj8o5VyqfLt7I9c9MI71BTcbd2IfGdapFHZIrh+JJBE8S3Fj+C4I2AudchD5etIEbR2fTtlEtxt7Qm0a1PQm4wxNPIqhiZncmLRLnXKl9uHA9Q8dMp12jWoy7sQ8Na1WNOiRXjsXTRvCmpKGSmktqWPhIWmTOuWJ9sGA9Q0dP5/jGtRnvScAlQDxnBAPDv/fELPPuo84dRe/OW8ePn51Ox2Z1GHt9b+rX9CTgjlw8Vxb7kNPORWjS3LXcMm4GGc3rMvq63tSrWSXqkFwFEdeVxZK6ABnA1/3TzGz0IbY5H3gUSAOGm9lDxZS5HLif4AzjczO7Mp64nKvo3pqzhlvHzaRLi3o8c10v6tXwJOASJ54ri+8DziRIBG8Q3K/4E+CgiUBSGvA4wf2OVwLTJE00s3kxZdoTVDedYmZbJDU5jOfhXIX1+uw13DZhJl1b1mPUdb2oW92TgEuseBqLfwicDaw1syFAV6DeIbbpBSw2s6Vmth+YAPQvUuZG4HEz2wJgZuvjiMm5Cu3Vz1dz24SZdE+vz+jre3sScEkRTyLYY2YFQJ6kusB6gltXlqQFsCJmfmW4LFYHoIOkTyVlhVVJzqW8/8xaxe0TZtKjVQNGXdeL2tXiqsl1rtTi+WRlS6pPcA+C6cBOYHKCYmhPUO3UEvhY0olmtjW2kKShwFCAVq1aJeCwzpVdL81YyV0vfE7PNg0ZMbgntTwJuCQq1adLkoAHwy/nJyS9BdQ1s9mH2HQVB541tAyXxVoJTDGzXOArSYsIEsO02EJm9hTwFEBmZqaVJm7nyqMXslfwixdn07fdMQy/NpOaVT0JuOQqVdVQeIvKN2Lml5UiCUDwZd5eUltJVYEBwMQiZV4hOBtAUiOCqqKlpYnLuYrmuWnL+cWLsznluEY8fW1PTwLuqIinjWCGpJ7x7NzM8oBbgbeB+cDzZjZX0gOSLg6LvQ1skjQP+AD4uZltiuc4zlUE46Ys55cvfsGpxzdi+LWZ1KiaFnVILkUo+LFfioLSAuB4IAfYRXhjGjM7KXnhFS8zM9Oys7OP9mGdS5oxWTn85pU5nNmxMU9c1YPqVTwJuMSTNN3MMosuj+e887wExuOcC42evIzf/mcuZ3dqwj+vOplqlT0JuKMrniEmcgDCC778zhfOJcCIT77igdfmcU7npjw+qLsnAReJUrcRSLpY0pfAV8BHwDLgzSTF5VyFN/y/S3ngtXmcd0JT/jnIzwRcdOJpLP490AdYFA5AdzaQlZSonKvgnvxoCX94fT7fO7EZj115MlUrx/Ov6FxixfPpyw1781SSVMnMPgC+1ejgnCvZPz9czINvLuDCk5rz6IDuVEnzJOCiFU9j8VZJtYH/As9KWk/Qe8g5V0r/eO9L/vLOIi7ueiyPXN6Vyp4EXBkQz6ewP7AH+BnwFrAE+H4SYnKuQvrbu4v4yzuLuKR7C08CrkyJp9fQLknNCEYU3Qy87Rd+OXdoZsZf31nE399fzGUnt+RPPzyJtEqKOiznvhZPr6EbgKnApQRDUmdJui5ZgTlXEZgZf560kL+/v5gf9fAk4MqmeNoIfg50LzwLkHQM8BkwIhmBOVfemRkPv7WQJz5awoCe6fzvJSdSyZOAK4PiSQSbgB0x8zvCZc65IsyMB99cwFMfL2VQ71b8vn8XTwKuzIonESwGpkj6D8G9hfsDsyXdCWBmjyQhPufKHTPjD6/P5+lPvuLqPq15oP8JBCO5O1c2xZMIloSPQv8J/9ZJXDjOlW9mxu9enceoz5Yx+DttuO/7GZ4EXJkXT6+h35W0XtI/zOynRx6Sc+WTmXHfxLmMnpzDdae05TcXdfYk4MqFRN714pQE7su5cmV/XgF3vzSbl2asYujp7bjngk6eBFy54bc/cu4I7diby4/HzuCTxRu545wO3Hb28Z4EXLniicC5I7Bu+14Gj5zGonU7+NMPT+LyzPRDb+RcGZPIROA/gVxKWbRuB4NHTGXbnlxGDO7JGR0aRx2Sc4clkYng0QTuy7kyLWvpJoaOzqZalTSeu6kvXVrUizok5w5bqROBpEzg10DrcLsD7llsZqOSEaBzZc1rs1dz53Of07JhDZ4Z0ov0hjWjDsm5IxLPGcGzBMNMfAEUlHYjSecTnC2kAcPN7KEi6wcD/wesChc9ZmbD44jLuaPCzHj6k6/4w+vzyWzdgOHXZlK/ZtWow3LuiMWTCDaY2cR4di4pDXgc6AesBKZJmmhm84oUfc7Mbo1n384dTfkFxh9en8fIT5dxQZdm/PWKblSv4reWdBVDPIngPknDgfeAfYULzeylErbpBSw2s6UAkiYQDE1RNBE4V2btzc3njudm8eactQw5pQ2/uTDDxw1yFUo8iWAI0AmowjdVQwaUlAhaACti5lcCvYspd5mk04FFwB1mtqKYMs4ddVt27efG0dlk52zh3gs7c8Np7aIOybmEiycR9DSzjkmI4VVgvJntk3QT8AxwVtFCkoYCQwFatWqVhDCcO9CKzbu5duRUVm7ew2NXdueik46NOiTnkiKee+V9Jikjzv2vAmKvsGnJN43CAJjZJjMrrGoaDvQobkdm9pSZZZpZZuPG3l/bJdecVdu49F+fsXHHPsZc38uTgKvQ4jkj6APMkvQVQRvBAd1HD2Ia0F5SW4IEMAC4MraApOZmtiacvRiYH0dMziXchwvX85NnZ9CgZlXG3dCb9k19gF1XscWTCM6Pd+dmlifpVuBtgu6jI8xsrqQHgOywF9Jtki4G8gjuhTw43uM4lyjPT1vBPS9/QYemdRg1pCdN61aPOiTnkk5mVvrCUlfgtHD2v2b2eVKiOoTMzEzLzs6O4tCugjIzHn3vS/727pec1r4R/xx0MnWqV4k6LOcSStJ0M8ssujyem9ffTnBRWZPwMVaS33/AlXu5+QXc/eIX/O3dL7n05BaMGNzTk4BLKfFUDV0P9DazXQCSHgYmA/9IRmDOHQ279uVxy7gZfLhwAz8963ju7NfBh5B2KSeeRCAgP2Y+Hx9x1JVjG3bs47pR05i7ehv/e8mJXNnbuyW71BRPIhhJcPP6l8P5HwBPJzwi546CJRt2MnjkVDbu2M+wazI5u3PTqENyLjLx3LP4EUkfAqeGi4aY2cykROVcEk3P2cz1z2STJjF+aB+6pdePOiTnInXIRCCpYczssvDx9Toz25z4sJxLjrfmrOX2CTNpXq86z1zXi9bH1Io6JOciV5ozgukEYwoJaAVsCafrA8uBtskKzrlEeuazZdz/6ly6tqzP09dmckztalGH5FyZcMhEYGZtASQNA142szfC+QsI2gmcK9MKCoyH31rAkx8v5ZzOTfnHwO7UqOpDSDtXKJ6xhvoUJgEAM3sT+E7iQ3Iucfbl5XP7c7N48uOlXNWnFU9e3cOTgHNFxNNraLWke4Gx4fwgYHXiQ3IuMbbtyeWmMdlkLd3ML8/vxM1ntPNrBJwrRjyJYCBwH1DYffTjcJlzZc7qrXsYPHIqX23cxd+u6MYPureIOiTnyqx4uo9uBm5PYizOJcT8NdsZPHIqu/flM2pIL045vlHUITlXppU6EUjqANwFtIndzsy+dRMZ56Ly6eKN3DxmOrWqVeb5m/vSuXndqENyrsyLp2roBeAJgpvH5B+irHNH3cszV/KLf8+mbaNajBrSi2Pr14g6JOfKhXgSQZ6Z/StpkTh3mMyMf320hD+9tZA+7Rry5NWZ1Kvho4c6V1rxJIJXJf2EoLG48NaS+JXFLkr5BcZ9E+cwNms5F3c9lv/70UlUq+zdQ52LRzyJ4Nrw789jlhnQLnHhOFd6e/bn89PxM3l3/jpuPuM4fnFeRypV8u6hzsUrnl5DPpSEKzM27dzH9c9k8/nKrTzQ/wSu6dsm6pCcK7fiOSNAUhcgA/j6Rq5mNjrRQTlXkpxNu7h2xFTWbNvLvwb14PwuzaIOyblyLZ7uo/cBZxIkgjeAC4BPAE8E7qiZtWIr14+aRoEZ427sQ4/WDaIOyblyL56xhn4InA2sNbMhQFeg3qE2knS+pIWSFku6u4Ryl0kySd+6sbJzAO/OW8eApyZTs1oaL/74O54EnEuQeBLBXjMrAPIk1QXWA+klbSApDXic4OwhAxgoKaOYcnUIrlqeEkc8LoU8OyWHoWOyad+kDi/9+BTaNa4ddUjOVRilSgQKRuqaLak+MIzgHgUzCG5eX5JewGIzW2pm+4EJQP9iyv0eeBjYW8q4XYowM/789kJ+/fIczujQmAlD+9C4jt9HwLlEKlUbgZmZpF5mthV4QtJbQF0zm32ITVsAK2LmVwK9YwtIOhlIN7PXJcV2TXUpbn9eAXe/NJuXZqxiYK90ft+/C5XT4jmJdc6VRjy9hmZI6mlm08xsWSIOLqkS8AgwuBRlhwJDAVq1apWIw7sybMfeXH48dgafLN7Inf068NOzjvchpJ1LkngSQW9gkKQcYBfB7SrNzE4qYZtVHNiO0DJcVqgO0AX4MPwnbwZMlHSxmWXH7sjMngKeAsjMzLQ44nblzLrtexk8chpfrtvB//3wJH6UWWJTlHPuCMWTCM47jP1PA9pLakuQAAYAVxauNLNtwNdjBEv6ELiraBJwqWPRuh0MHjGVbXtyeXpwT87o0DjqkJyr8OK5sjgn3p2bWZ6kW4G3gTRghJnNlfQAkG1mE+Pdp6u4spZuYujobKpVSeO5m/rSpcUheyc75xIgriuLD0d4n+M3iiz77UHKnpnseFzZ9Nrs1dz53OekN6zBqCG9SG9YM+qQnEsZSU8EzpXEzHj6k6/4w+vz6dmmAcOuyaR+zapRh+VcSvFE4CKTX2D84fV5jPx0Gd87sRmPXN6N6lV8CGnnjjZPBC4Se3PzueO5Wbw5Zy3XndKWey/s7ENIOxcRTwTuqNuyaz83js5m+vIt3HthZ244zW9p4VyUPBG4o2rF5t1cO3IqK7fs4bGBJ3PhSc2jDsm5lOeJwB01c1ZtY/DIaezPy2fs9b3p1bZh1CE55/BE4I6SDxeu5yfPzqBBzaqMv7E37ZvWiTok51zIE4FLuuenreCel7+gY9M6jBzSk6Z1qx96I+fcUeOJwCWNmfHoe1/yt3e/5LT2jfjnoJOpU71K1GE554rwROCSIje/gHtfnsNz2Su47OSWPHTZiVTxIaSdK5M8EbiE27Uvj1vGzeDDhRu47azjuaNfBx9C2rkyzBOBS6gNO/Zx3ahpzFuznQcvPZGBvfzeEc6VdZ4IXMIs2bCTwSOnsnHHfoZd04OzOjWNOiTnXCl4InAJMT1nM9c/k02axIShfeiaXj/qkJxzpeSJwB2xt+as5fYJMzm2fg1GDelJ62NqRR2Scy4OngjcEXnms2Xc/+pcuqXX5+lre9Kwlg8h7Vx544nAHZaCAuPhtxbw5MdL6ZfRlL8P6E6Nqj6EtHPlkScCF7d9efnc9cJsXv18NVf3ac39F59Amg8h7Vy55YnAxWXbnlxuGpNN1tLN3H1BJ246vZ1fI+BcOeeJwJXa6q17GDxyKl9t3MWjA7rRv1uLqENyziWAJwJXKvPXbGfwyKns3pfPM0N68Z3jG0UdknMuQZI++Iuk8yUtlLRY0t3FrL9Z0heSZkn6RFJGsmNy8fl08UYuf2IyQrzw476eBJyrYJKaCCSlAY8DFwAZwMBivujHmdmJZtYN+BPwSDJjcvF5eeZKBo+cyrH1a/DyLd+hU7O6UYfknEuwZFcN9QIWm9lSAEkTgP7AvMICZrY9pnwtwJIckzuE9Tv28t789Uyau5YPFm6gb7tjeOLqHtSr4UNIO1cRJTsRtABWxMyvBHoXLSTpFuBOoCpwVnE7kjQUGArQqpUPZJZoSzbs5J1565g0dy0zV2zFDNIb1uCW7x7HbWe3p1plv0bAuYqqTDQWm9njwOOSrgTuBa4tpsxTwFMAmZmZftZwhAoKjFkrt3795b9kwy4AurSoyx3ndKBfRlM6NavjXUOdSwHJTgSrgPSY+ZbhsoOZAPwrqRGlsH15+Xy2ZBPvzFvHO/PWsWHHPtIqiT7tGnJN3zack9GUFvVrRB2mc+4oS3YimAa0l9SWIAEMAK6MLSCpvZl9Gc5eCHyJS5hte3L5cOF6Js1dx4cL17Nrfz61qqZxRsfGnJvRjO92bEK9ml7371wqS2oiMLM8SbcCbwNpwAgzmyvpASDbzCYCt0o6B8gFtlBMtZCLz5pte8Iqn3VkLd1EXoHRqHY1Lu52LOdmNKPvccdQvYrX+TvnAjIrf9XtmZmZlp2dHXUYZYaZsWjdTibNXcs789cxe+U2ANo1qkW/E5pybkYzuqfXp5KPB+RcSpM03cwyiy4vE43FLn75Bcb0nC1MmruWSfPWsXzzbgC6t6rPL87vyLkZzTi+Se2Io3TOlQeeCMqRPfvz+WTxRibNXct7C9azedd+qqZV4jvHH8NNZ7SjX+emNKlbPeownXPljCeCMm7zrv28Nz/o5fPxlxvYm1tAneqVOatTE/plNOWMDo2pU90be51zh88TQRm0fNNuJs1byzvz1jFt2WYKDJrVrc7lmemcm9GMXm0bUrVy0oeJcs6lCE8EZYCZMXf19q/r+xes3QFAx6Z1uOW7x3NuRjO6tKjrF3c555LCE0FEcvMLmPrV5qCnz7x1rN62l0qCzNYNuffCzvTLaOo3gXfOHRWeCI6infvy+HjRBibNXcv7C9azfW8e1SpX4rT2jflZvw6c3akJx9SuFnWYzrkU44kgyWJH8vx0ySb25xXQoGYV+mU049wTmnJa+0bUrOpvg3MuOv4NlAQHG8nz6j6t6ZfRlMzWDaic5o29zrmywRNBAvhIns658swTwWHykTydcxWFJ4I4fD2S57x1fLjAR/J0zlUMnggOoXAkz3fmrWPyEh/J0zlX8XgiKKKkkTyvP62tj+TpnKtwPBHgI3k651JbyiYCH8nTOecCKZUIfCRP55z7tpRKBH+ZtJBnpyz3kTydcy5GSiWC605ty4CerXwkT+eci5FSieC4xt7g65xzRSW9TkTS+ZIWSlos6e5i1t8paZ6k2ZLek9Q62TE555z7RlITgaQ04HHgAiADGCgpo0ixmUCmmZ0E/Bv4UzJjcs45d6BknxH0Ahab2VIz2w9MAPrHFjCzD8xsdzibBbRMckzOOediJDsRtABWxMyvDJcdzPXAm0mNyDnn3AHKTGOxpKuATOCMg6wfCgwFaNWq1VGMzDnnKrZknxGsAtJj5luGyw4g6Rzg18DFZravuB2Z2VNmlmlmmY0bN05KsM45l4qSnQimAe0ltZVUFRgATIwtIKk78CRBElif5Hicc84VITNL7gGk7wF/A9KAEWb2R0kPANlmNlHSu8CJwJpwk+VmdvEh9rkByDnMkBoBGw9z22TyuOLjccXH44pfWY3tSOJqbWbfqlJJeiIoayRlm1lm1HEU5XHFx+OKj8cVv7IaWzLi8kF2nHMuxXkicM65FJeKieCpqAM4CI8rPh5XfDyu+JXV2BIeV8q1ETjnnDtQKp4ROOeci+GJwDnnUlzKJAJJIyStlzQn6lhiSUqX9EE4FPdcSbdHHROApOqSpkr6PIzrd1HHFEtSmqSZkl6LOpZCkpZJ+kLSLEnZUcdTSFJ9Sf+WtEDSfEl9y0BMHcPXqfCxXdLPoo4LQNId4Wd+jqTxksrEzcsl3R7GNDfRr1XKtBFIOh3YCYw2sy5Rx1NIUnOguZnNkFQHmA78wMzmRRyXgFpmtlNSFeAT4HYzy4oyrkKS7iQYm6qumV0UdTwQJAKCIdXL1EVIkp4B/mtmw8Mr/Gua2daIw/paOFz9KqC3mR3uhaKJiqUFwWc9w8z2SHoeeMPMRkUcVxeC0Zt7AfuBt4CbzWxxIvafMmcEZvYxsDnqOIoyszVmNiOc3gHMp+QRWo8KC+wMZ6uEjzLxq0FSS+BCYHjUsZR1kuoBpwNPA5jZ/rKUBEJnA0uiTgIxKgM1JFUGagKrI44HoDMwxcx2m1ke8BFwaaJ2njKJoDyQ1AboDkyJOBTg6+qXWcB64B0zKxNxEQxZ8gugIOI4ijJgkqTp4Wi5ZUFbYAMwMqxKGy6pVtRBFTEAGB91EABmtgr4M7CcYNibbWY2KdqoAJgDnCbpGEk1ge9x4ICeR8QTQRkhqTbwIvAzM9sedTwAZpZvZt0IRo3tFZ6eRkrSRcB6M5sedSzFONXMTia4I98tYXVk1CoDJwP/MrPuwC7gW7eMjUpYVXUx8ELUsQBIakBw86y2wLFArXCI/EiZ2XzgYWASQbXQLCA/Ufv3RFAGhHXwLwLPmtlLUcdTVFiV8AFwfsShAJwCXBzWx08AzpI0NtqQAuGvScJRdF8mqM+N2kpgZczZ3L8JEkNZcQEww8zWRR1I6BzgKzPbYGa5wEvAdyKOCQAze9rMepjZ6cAWYFGi9u2JIGJho+zTwHwzeyTqeApJaiypfjhdA+gHLIg0KMDM7jGzlmbWhqBK4X0zi/wXm6RaYWM/YdXLuQSn85Eys7XACkkdw0VnA5F2RChiIGWkWii0HOgjqWb4v3k2Qbtd5CQ1Cf+2ImgfGJeofZeZO5Qlm6TxwJlAI0krgfvM7OloowKCX7hXA1+E9fEAvzKzN6ILCYDmwDNhj45KwPNmVma6apZBTYGXg+8OKgPjzOytaEP62k+BZ8NqmKXAkIjjAb5OmP2Am6KOpZCZTZH0b2AGkAfMpOwMNfGipGOAXOCWRDb6p0z3Ueecc8XzqiHnnEtxngiccy7FeSJwzrkU54nAOedSnCcCl1IkDZZ0bNRxHA5JZ0oqE33aXcXiicClmsEEV4wmRTg+TbKcSZwXNyU5HldBePdRV+6F/dGfJxgKIw34PbAYeASoDWwkSACnAKMIRrrcA/Q1sz3F7G9ZuL8LwnJXmtliSd8H7gWqApuAQWa2TtL9wHFAO4ILku4BxgCFY/rcamafSToT+B2wFTgxPMYXwO1ADYJRZ5dIagw8AbQKt/9ZGHMWwbACGwiuDVhQtJyZfVo0HjMbGMfL6VKRmfnDH+X6AVwGDIuZrwd8BjQO568ARoTTHxIMFV3S/pYBvw6nrwFeC6cb8M2PpxuAv4TT9xMMH14jnK8JVA+n2wPZ4fSZBEmgOVCN4Mv9d+G624G/hdPjCMYtguBLfn7Mce6KibOkcl/H4w9/HOrhp42uIvgC+Iukh4HXCMZh6QK8E17pm0YwkmQ8xsf8/Ws43RJ4LryHRFXgq5jyE+2bs4sqwGOSuhH8gu8QU26ama0BkLSEYBCxwufw3XD6HCAjjB2gbjgoYVEllYuNx7kSeSJw5Z6ZLZJ0MsHQvH8A3gfmmtmR3InLipn+B/CImU0Mq3nujymzK2b6DmAd0JWgHW5vzLp9MdMFMfMFfPP/WAnoY2ax2xHzhU8pyu0qWti5g/HGYlfuhb2AdpvZWOD/gN5A48JbMkqqIumEsPgOoE4pdntFzN/J4XQ9guocgGtL2LYesMbMCgjGkUor7XMJTSJoAwAgPLOAb8d+sHLOxcUTgasITgSmhoP23Qf8Fvgh8LCkzwnGbi/sbTMKeCK8T26NEvbZQNJsgrr7O8Jl9wMvSJpO0AB9MP8Erg2P3Yn4f53fBmRKmi1pHnBzuPxV4JIw9tNKKOdcXLzXkHNFlNV7DzuXLH5G4JxzKc4bi13KkvQywS0JY/3SgpveOJcyvGrIOedSnFcNOedcivNE4JxzKc4TgXPOpThPBM45l+I8ETjnXIrzROCccynu/wH5PtHnD/+U/wAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plot_by_id(datasaver.dataset.run_id);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now try the same using the `MeasurementLoop`." + ] + }, + { + "cell_type": "code", + "execution_count": 106, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Measurement error RuntimeError(Wrong measurement at action_indices (0, 0). Expected: fixed_parameter. Received: random_parameter) - varied_order_measurement_loop\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 34. \n" + ] + }, + { + "ename": "RuntimeError", + "evalue": "Wrong measurement at action_indices (0, 0). Expected: fixed_parameter. Received: random_parameter", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mRuntimeError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m~\\AppData\\Local\\Temp/ipykernel_29980/2330389870.py\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 2\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mset_v\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mSweep\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mrange\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m11\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'sweep_values'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 3\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mset_v\u001b[0m \u001b[1;33m%\u001b[0m \u001b[1;36m2\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 4\u001b[1;33m \u001b[0mmsmt\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmeasure\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mrandom_parameter\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 5\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 6\u001b[0m \u001b[0mmsmt\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmeasure\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mfixed_parameter\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32mc:\\users\\serwan\\documents\\github\\qcodes_new\\qcodes\\dataset\\measurement_loop.py\u001b[0m in \u001b[0;36mmeasure\u001b[1;34m(self, measurable, name, label, unit, timestamp, **kwargs)\u001b[0m\n\u001b[0;32m 866\u001b[0m \u001b[1;31m# TODO Incorporate kwargs name, label, and unit, into each of these\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 867\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mmeasurable\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mParameter\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 868\u001b[1;33m result = self._measure_parameter(\n\u001b[0m\u001b[0;32m 869\u001b[0m \u001b[0mmeasurable\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mname\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mname\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mlabel\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mlabel\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0munit\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0munit\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 870\u001b[0m )\n", + "\u001b[1;32mc:\\users\\serwan\\documents\\github\\qcodes_new\\qcodes\\dataset\\measurement_loop.py\u001b[0m in \u001b[0;36m_measure_parameter\u001b[1;34m(self, parameter, name, label, unit, **kwargs)\u001b[0m\n\u001b[0;32m 647\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 648\u001b[0m \u001b[1;31m# Ensure measuring parameter matches the current action_indices\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 649\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_verify_action\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0maction\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mparameter\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mname\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mname\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0madd_if_new\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;32mTrue\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 650\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 651\u001b[0m \u001b[1;31m# Get parameter result\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32mc:\\users\\serwan\\documents\\github\\qcodes_new\\qcodes\\dataset\\measurement_loop.py\u001b[0m in \u001b[0;36m_verify_action\u001b[1;34m(self, action, name, add_if_new)\u001b[0m\n\u001b[0;32m 616\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0maction_names\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0maction_indices\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mname\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 617\u001b[0m \u001b[1;32melif\u001b[0m \u001b[0mname\u001b[0m \u001b[1;33m!=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0maction_names\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0maction_indices\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 618\u001b[1;33m raise RuntimeError(\n\u001b[0m\u001b[0;32m 619\u001b[0m \u001b[1;34mf\"Wrong measurement at action_indices {self.action_indices}. \"\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 620\u001b[0m \u001b[1;34mf\"Expected: {self.action_names[self.action_indices]}. Received: {name}\"\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;31mRuntimeError\u001b[0m: Wrong measurement at action_indices (0, 0). Expected: fixed_parameter. Received: random_parameter" + ] + } + ], + "source": [ + "with MeasurementLoop('varied_order_measurement_loop') as msmt:\n", + " for set_v in Sweep(range(11), 'sweep_values'):\n", + " if set_v % 2:\n", + " msmt.measure(random_parameter)\n", + " else:\n", + " msmt.measure(fixed_parameter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lo and behold, an error appeared. This is because it expects the first measurement to be `fixed_parameter`, which was measured during the first iteration, but instead the second iteration it measures `random_parameter`.\n", + "\n", + "This problem can be solved by explicitly telling the `MeasurementLoop` which is the first or second measurement by adding `msmt.skip`.\n", + "In this example, `random_parameter` has a `msmt.skip()` before it, indicating that another parameter is usually measured first (though not this time) and so it's actually the second parameter being measured." + ] + }, + { + "cell_type": "code", + "execution_count": 110, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 36. \n" + ] + } + ], + "source": [ + "with MeasurementLoop('varied_order_measurement_loop') as msmt:\n", + " for set_v in Sweep(range(11), 'sweep_values'):\n", + " if set_v % 2:\n", + " msmt.skip()\n", + " msmt.measure(random_parameter)\n", + " else:\n", + " msmt.measure(fixed_parameter)\n", + " msmt.skip()" + ] + }, + { + "cell_type": "code", + "execution_count": 109, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plot_by_id(msmt.dataset.run_id);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A `break` statement is the second situation in which the `MeasurementLoop` needs an explicit signal to ensure the measurement order is adhered to. The reason is because unlike a context manager, a for-loop has no way of knowing when the loop has been prematurely exited, and so it won't be able to perform the necessary actions when exiting a `Sweep`." + ] + }, + { + "cell_type": "code", + "execution_count": 113, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 39. \n", + "set_v=0, continuing measurement\n", + "set_v=1, continuing measurement\n", + "set_v=2, continuing measurement\n", + "set_v=3, continuing measurement\n", + "set_v=4, exiting prematurely using `msmt.step_out`\n" + ] + } + ], + "source": [ + "with MeasurementLoop('varied_order_measurement_loop') as msmt:\n", + " for set_v in Sweep(range(11), 'sweep_values'):\n", + " if (set_v+1) % 5:\n", + " print(f'{set_v=}, continuing measurement')\n", + " msmt.measure(random_parameter)\n", + " else:\n", + " print(f'{set_v=}, exiting prematurely using `msmt.step_out`')\n", + " msmt.step_out()\n", + " break" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this case we see that the `break` statement is preceded by `msmt.step_out`. This indicates to the `MeasurementLoop` that it has to take the necessary actions because the Sweep is exited." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## `Sweep` functionalities" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Additional features" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Value masking" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sweeping/measuring without a parameter" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Measuring same parameter multiple times" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.9.5 ('base')", + "language": "python", + "name": "python3" + }, + "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.9.5" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "19d1d53a962d236aa061289c2ac16dc8e6d9648c89fe79f459ae9a3493bc67b4" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 783c48dd596b73ec8c2d92ed54bbd69022aab76c Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 3 Aug 2022 20:46:47 +0200 Subject: [PATCH 034/122] finished tutorial --- docs/examples/DataSet/MeasurementLoop.ipynb | 498 +++++++++++++++++++- qcodes/dataset/measurement_loop.py | 17 + 2 files changed, 514 insertions(+), 1 deletion(-) diff --git a/docs/examples/DataSet/MeasurementLoop.ipynb b/docs/examples/DataSet/MeasurementLoop.ipynb index 35f32f10a56..8481e2bf770 100644 --- a/docs/examples/DataSet/MeasurementLoop.ipynb +++ b/docs/examples/DataSet/MeasurementLoop.ipynb @@ -833,6 +833,302 @@ "## `Sweep` functionalities" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Sweeping over a sequence of values in a `MeasurementLoop` is done using the `Sweep` object. It can sweep over an explicit sequence of values, or it can be given arguments to generate a sequence from." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sweeping a parameter" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A parameter can be swept over by passing the parameter as the first argument. Here we create a sweep of parameter \"set_parameter\" over values \"[1, 2, 3, 4]\"" + ] + }, + { + "cell_type": "code", + "execution_count": 123, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Sweep(parameter=set_parameter, length=4)" + ] + }, + "execution_count": 123, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "parameter_sweep = Sweep(set_parameter, [1,2,3,4])\n", + "parameter_sweep" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this case, the parameter value is automatically changed during the measurement:" + ] + }, + { + "cell_type": "code", + "execution_count": 125, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 41. \n", + "set_parameter()=1\n", + "set_parameter()=2\n", + "set_parameter()=3\n", + "set_parameter()=4\n" + ] + } + ], + "source": [ + "with MeasurementLoop('sweep_set_parameter_measurement') as msmt:\n", + " for val in parameter_sweep:\n", + " print(f'{set_parameter()=}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sweeping without a parameter" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is also possible to create a `Sweep` without a parameter. In this case, no parameter value is updated. In this case it's necessary to pass along a \"name\"" + ] + }, + { + "cell_type": "code", + "execution_count": 129, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Sweep('sweep_without_parameter', length=3)" + ] + }, + "execution_count": 129, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Sweep([1,2,3], name='sweep_without_parameter')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Generating a sequence" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the previous example we saw how you can create a sweep out of a pre-existing sequence. The `Sweep` also has convenient methods to generate a sequence using (keyword) arguments. \n", + "The following keyword arguments are identical to `np.linspace`" + ] + }, + { + "cell_type": "code", + "execution_count": 134, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10.])" + ] + }, + "execution_count": 134, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sweep = Sweep(set_parameter, start=0, stop=10, num=11)\n", + "sweep.sequence" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also give a `step`, in which case it behaves like `np.arange`, with the exception that here the last value is included" + ] + }, + { + "cell_type": "code", + "execution_count": 136, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 0, 2, 4, 6, 8, 10])" + ] + }, + "execution_count": 136, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sweep = Sweep(set_parameter, start=0, stop=10, step=2)\n", + "sweep.sequence" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One can also use the current value of a \"set_parameter\" to generate a sequence. Here we tell it to create 11 points in a range of 5 below to above it's current value" + ] + }, + { + "cell_type": "code", + "execution_count": 137, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([-3., -2., -1., 0., 1., 2., 3., 4., 5., 6., 7.])" + ] + }, + "execution_count": 137, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "set_parameter(2)\n", + "sweep = Sweep(set_parameter, around=5, num=11)\n", + "sweep.sequence" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or here we choose 11 points from whatever it's current value is to 12" + ] + }, + { + "cell_type": "code", + "execution_count": 140, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.])" + ] + }, + "execution_count": 140, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "set_parameter(2)\n", + "sweep = Sweep(set_parameter, stop=12, num=11)\n", + "sweep.sequence" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sweep arguments" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The most common types of sweeps can also be created without using keyword arguments. For example:" + ] + }, + { + "cell_type": "code", + "execution_count": 144, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Sweep(parameter=set_parameter, length=11)" + ] + }, + "execution_count": 144, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Sweep(set_parameter, 0, 10, 11)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "is equivalent to" + ] + }, + { + "cell_type": "code", + "execution_count": 145, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Sweep(parameter=set_parameter, length=11)" + ] + }, + "execution_count": 145, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Sweep(set_parameter, start=0, stop=10, num=11)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A full list of argument combinations can be found in the docstring of `Sweep.transform_args_to_kwargs`" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -851,7 +1147,156 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Sweeping/measuring without a parameter" + "The `MeasurementLoop` also provides the ability to mask the value of an object during the measurement. For example" + ] + }, + { + "cell_type": "code", + "execution_count": 146, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Initial value get_parameter()=2\n", + "Starting experimental run with id: 42. \n", + "Masked value get_parameter()=9\n", + "Value after measurement finished: get_parameter()=2\n" + ] + } + ], + "source": [ + "get_parameter = ManualParameter('get_parameter', initial_value=2)\n", + "print(f'Initial value {get_parameter()=}')\n", + "\n", + "with MeasurementLoop('masking_measurement') as msmt:\n", + " msmt.mask(get_parameter, 9)\n", + " \n", + " print(f'Masked value {get_parameter()=}')\n", + "\n", + " for val in Sweep(set_parameter, range(5)):\n", + " msmt.measure(get_parameter)\n", + "\n", + "print(f'Value after measurement finished: {get_parameter()=}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This can be especially useful when measurements are encapsulated in functions, as it allows to set parameters to specific values during the measurement, knowing that it will be reset after.\n", + "\n", + "The unmasking of a parameter happens after a measurement is complete, even if the measurement fails." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Masking dictionaries and object attributes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We just saw that it's possible to mask a parameter value. It is also possible to mask two other elements:\n", + "- keys in dictionaries\n", + "- attributes of objects\n", + "\n", + "Here we show the two examples" + ] + }, + { + "cell_type": "code", + "execution_count": 149, + "metadata": {}, + "outputs": [], + "source": [ + "# First create a dummy class\n", + "class MyObject:\n", + " object_attribute = 42\n" + ] + }, + { + "cell_type": "code", + "execution_count": 154, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Initial object value my_object.object_attribute=42\n", + "Initial dictionary d={'key1': 12, 'key2': 13, 'key3': 14}\n", + "Starting experimental run with id: 44. \n", + "Masked object value my_object.object_attribute=999\n", + "Masked dictionary d={'key1': 12, 'key2': 999, 'key3': 14}\n", + "Final object value my_object.object_attribute=42\n", + "Final dictionary d={'key1': 12, 'key2': 13, 'key3': 14}\n" + ] + } + ], + "source": [ + "\n", + "my_object= MyObject()\n", + "print(f'Initial object value {my_object.object_attribute=}')\n", + "\n", + "d = dict(key1=12, key2=13, key3=14)\n", + "print(f'Initial dictionary {d=}')\n", + "\n", + "with MeasurementLoop('masking_dictionary_and_object') as msmt:\n", + " msmt.mask(my_object, object_attribute=999)\n", + " msmt.mask(d, key2=999)\n", + "\n", + " print(f'Masked object value {my_object.object_attribute=}')\n", + " print(f'Masked dictionary {d=}')\n", + "\n", + "print(f'Final object value {my_object.object_attribute=}')\n", + "print(f'Final dictionary {d=}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Measuring without a parameter" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Most previous examples showed how we can measure a `Parameter` in a `MeasurementLoop`. However, this is not a requirement. Just as one can create a `Sweep` without a parameter, so can one also measure things that are not a parameter." + ] + }, + { + "cell_type": "code", + "execution_count": 157, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 47. \n" + ] + } + ], + "source": [ + "with MeasurementLoop('measure_non_parameters') as msmt:\n", + " for k in Sweep(range(5), 'sweep'):\n", + " msmt.measure(42, 'measure_value')\n", + " msmt.measure({'val1': 1, 'val2': 2}, 'measure_dict')\n", + "\n", + " # One can also measure a function that returns a dict\n", + " def random_int(min_val=1, max_val=50):\n", + " return {\n", + " 'val1': np.random.randint(min_val, max_val),\n", + " 'val2': np.random.randint(min_val, max_val)\n", + " }\n", + " msmt.measure(random_int, 'measure_callable')" ] }, { @@ -860,6 +1305,57 @@ "source": [ "### Measuring same parameter multiple times" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One feature of the `MeasurementLoop` that is not possible in the original `Measurement` is that the same parameter can be swept/measured at multiple different points:" + ] + }, + { + "cell_type": "code", + "execution_count": 160, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting experimental run with id: 49. \n" + ] + }, + { + "data": { + "text/plain": [ + "measure_same_parameter #49@C:\\Users\\Serwan\\experiments.db\n", + "---------------------------------------------------------\n", + "sweep_parameter - numeric\n", + "random_parameter - numeric\n", + "random_parameter_1 - numeric" + ] + }, + "execution_count": 160, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "with MeasurementLoop('measure_same_parameter') as msmt:\n", + " for k in Sweep(range(10), 'sweep_parameter'):\n", + " msmt.measure(random_parameter)\n", + " msmt.measure(random_parameter)\n", + "msmt.dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As can be seen, this creates two different measurement arrays. The second measurement automatically appends an index to distinguish its name from the original measurement array.\n", + "\n", + "This is a useful feature especially when encapsulating measurements in functions. In this case it could very well occur that the same parameter is measured at multiple different locations" + ] } ], "metadata": { diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 58f45fcc7b8..e7fd9a69ba0 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1258,6 +1258,23 @@ def __init__(self, sequence, name=None, label=None, unit=None, parameter=None, r for value in self.sequence: self.parameter.validate(value) + def __repr__(self): + components = [] + + # Add parameter or name + if self.parameter is not None: + components.append(f'parameter={self.parameter}') + elif self.name is not None: + components.append(f"'{self.name}'") + + # Add number of elements + num_elems = str(len(self.sequence)) if self.sequence is not None else 'unknown' + components.append(f'length={num_elems}') + + # Combine components + components_str = ', '.join(components) + return f'Sweep({components_str})' + def __iter__(self): if threading.current_thread() is not MeasurementLoop.measurement_thread: raise RuntimeError( From 06cd4d817aeda7482dcb87d5a6e2d16a61ecabad Mon Sep 17 00:00:00 2001 From: Serwan Date: Thu, 4 Aug 2022 15:35:11 +0200 Subject: [PATCH 035/122] slightly improve sweep execution --- qcodes/dataset/measurement_loop.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index e7fd9a69ba0..b5c310f3864 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1399,7 +1399,7 @@ def execute( name: str = None, measure_params: Iterable = None, repetitions: int = 1, - sweep: Union[Iterable, AbstractSweep] = None + sweep: Union[Iterable, BaseSweep] = None ): # Get "measure_params" from station if not provided if measure_params is None: @@ -1414,7 +1414,7 @@ def execute( # Ensure sweeps is a list if isinstance(sweep, BaseSweep): sweeps = [sweep] - elif isinstance(sweep, Iterable): + elif isinstance(sweep, (list, tuple)): sweeps = list(sweep) # Add repetition as a sweep if > 1 @@ -1425,7 +1425,7 @@ def execute( # Determine "name" if not provided from sweeps if name is None: dimensionality = 1 + len(sweep) - sweep_names = [sweep.name for sweep in sweeps] + [str(self.name)] + sweep_names = [str(sweep.name) for sweep in sweeps] + [str(self.name)] name = f'{dimensionality}D_sweep_' + '_'.join(sweep_names) with MeasurementLoop(name) as msmt: From f441802e833876bf14579bc0845a15e9cd2d778a Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Fri, 5 Aug 2022 17:40:32 +0200 Subject: [PATCH 036/122] Forgot to add self as sweep in Sweep.execute --- qcodes/dataset/measurement_loop.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index b5c310f3864..ec151516e5b 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1411,20 +1411,26 @@ def execute( ) measure_params = station.measure_params - # Ensure sweeps is a list + + # Create list of sweeps if isinstance(sweep, BaseSweep): sweeps = [sweep] elif isinstance(sweep, (list, tuple)): sweeps = list(sweep) + elif sweep is None: + sweeps = [] # Add repetition as a sweep if > 1 if repetitions > 1: repetition_sweep = BaseSweep(range(repetitions), name='repetition') sweeps = [repetition_sweep] + sweeps + + # Add self as innermost sweep + sweeps += [self] # Determine "name" if not provided from sweeps if name is None: - dimensionality = 1 + len(sweep) + dimensionality = 1 + len(sweeps) sweep_names = [str(sweep.name) for sweep in sweeps] + [str(self.name)] name = f'{dimensionality}D_sweep_' + '_'.join(sweep_names) From 24f5da3b9a742e36d3ae670f2d53d01d88254a2c Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Fri, 5 Aug 2022 17:40:46 +0200 Subject: [PATCH 037/122] fixed issue with self-referencing type-hint --- qcodes/dataset/measurement_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index ec151516e5b..43162f4ddc0 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1399,7 +1399,7 @@ def execute( name: str = None, measure_params: Iterable = None, repetitions: int = 1, - sweep: Union[Iterable, BaseSweep] = None + sweep: Union[Iterable, 'BaseSweep'] = None ): # Get "measure_params" from station if not provided if measure_params is None: From 14a283532b698fb276f61c4548d82cd07e09db16 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 5 Aug 2022 15:41:12 +0000 Subject: [PATCH 038/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 43162f4ddc0..1fc0f24cef1 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1424,13 +1424,13 @@ def execute( if repetitions > 1: repetition_sweep = BaseSweep(range(repetitions), name='repetition') sweeps = [repetition_sweep] + sweeps - + # Add self as innermost sweep sweeps += [self] # Determine "name" if not provided from sweeps if name is None: - dimensionality = 1 + len(sweeps) + dimensionality = 1 + len(sweeps) sweep_names = [str(sweep.name) for sweep in sweeps] + [str(self.name)] name = f'{dimensionality}D_sweep_' + '_'.join(sweep_names) From 3b9359a6610ec7381e26dd619b7b4423eb2a8628 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 9 Aug 2022 17:23:09 +0200 Subject: [PATCH 039/122] Added MeasurementLoop, Sweep to qcodes.dataset --- docs/examples/DataSet/MeasurementLoop.ipynb | 123 +++++++++++--------- qcodes/dataset/__init__.py | 3 + 2 files changed, 72 insertions(+), 54 deletions(-) diff --git a/docs/examples/DataSet/MeasurementLoop.ipynb b/docs/examples/DataSet/MeasurementLoop.ipynb index 8481e2bf770..dede2b3b93d 100644 --- a/docs/examples/DataSet/MeasurementLoop.ipynb +++ b/docs/examples/DataSet/MeasurementLoop.ipynb @@ -37,20 +37,23 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import time\n", "\n", - "from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep\n", - "from qcodes import (\n", - " ManualParameter, \n", - " Parameter,\n", + "from qcodes.dataset import (\n", + " MeasurementLoop, \n", + " Sweep,\n", " initialise_or_create_database_at, \n", " load_or_create_experiment\n", - ")" + ")\n", + "from qcodes.instrument import Parameter, ManualParameter\n", + "\n", + "initialise_or_create_database_at('database.db')\n", + "load_or_create_experiment('measurement_loop_experiment');" ] }, { @@ -69,7 +72,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "random_parameter()=0.9198744153995131\n" + "random_parameter()=0.42399278190478207\n" ] } ], @@ -88,14 +91,14 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Starting experimental run with id: 8. \n", + "Starting experimental run with id: 2. \n", "Finished measurement\n" ] } @@ -148,14 +151,14 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Starting experimental run with id: 10. Using 'qcodes.dataset.dond'\n" + "Starting experimental run with id: 3. Using 'qcodes.dataset.dond'\n" ] } ], @@ -173,14 +176,14 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Starting experimental run with id: 11. Using 'qcodes.dataset.do1d'\n" + "Starting experimental run with id: 4. Using 'qcodes.dataset.do1d'\n" ] } ], @@ -212,14 +215,14 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Starting experimental run with id: 12. \n" + "Starting experimental run with id: 5. \n" ] } ], @@ -269,14 +272,14 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Starting experimental run with id: 19. \n" + "Starting experimental run with id: 6. \n" ] } ], @@ -301,12 +304,12 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -318,7 +321,7 @@ }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXkAAAEXCAYAAABI/TQXAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAABSCUlEQVR4nO2dd5wcd3n/38/u3u71IumKuixZxSquMpZxL1i2qaEFU+0ADimUFFryI5gQICRAIIEETLBJgJiAAUPASDbggovcLVm9Wbba3UknXW9bnt8fM7M3u7f1bud2b+/7fr32dbszszPf2bn97DOf7/N9vqKqGAwGg6E88RW7AQaDwWDwDiPyBoPBUMYYkTcYDIYyxoi8wWAwlDFG5A0Gg6GMMSJvMBgMZYwRecO0R0T+RkT+s9jtKAVEZKOI3FPsdhQbEblNRL6f47Y/EZEbvG5TsTAiX0BE5JCIDIlIv4i0i8h3RaR2Co57VESqRORqEflp0rrPisgLIhIRkduS1omI/K2IvCwivSLyQxGpz/GYS0RE7XN1P/6wgKeWE6r6eVV931QfV0RuFpFHsmzzoIhMum0icqWIHMlh088B/+h6n4rImQU4fs6iOQ35IvAPxW6EVxiRLzyvVdVa4FzgPOCTXh5MRBYCXao6BFwAPJu0yX7gY8CvUrz93cC7gEuAeUAV8G95NqFRVWtdj//N8/2TQkQCU3m8UkZELgQaVHVLsdsynVDVJ4F6EVlf7LZ4gRF5j1DVdmAzltinjMTsyP9a+/ltIvIjEflvEekTkR05/tOtB55xPU8QeVX9L1X9NdCX4r2vBb6jqodVtR8rovlDEanO/UzHIyJBEXleRD5ov/aLyKMi8nf269tE5G4R+V/7XJ8VkXNc759n30KfEJEXReRDrnXOe78vIr3Aze4o03WHcYuIHBaR0yLyARG5UES2iUi3iHw9qb1/JCK77G03i8hi1zq137/Pfu837Dugs4BvAhfbdzDdKT6HzwGXAV+3t/m6vXyViNwvIqdEZI+IvNX1nhtFZKf9uRwVkb8WkRrg18A81x3TvBQf/Q3AQ659PWw/3eq+yxKR19jXp1tEHhORs13v+bh93D67bdeIyPXA32D9b/SLyNYMl9+5wzlo7+NFEXmHvXyZiPxORLpE5KSI/EBEGl3vOyQiH7Wv04CIfEdEWkXk1/a+fiMiTUnX+VYROSYix0XkrzO0aYN9rt0islVErkza5EHg1ZnOa9qiquZRoAdwCLjWfr4AeAH4mv36SuBIhu1vA4aBGwE/8AVgS4ZjfRrott8zaD+PAj32c3/S9t8HbktadjfwMdfrSwAFzsnhXJfY2wbSrF8LnAbOAv4W2OK0yT7XMPBmoAL4a+BF+7kP60fr74AgsBQ4CGxMeu8b7G2r7GXfT2rXN4FK4Dr7M7oHaAHmA53AFfb2r8e62zkLCAD/D3jMdR4K/BJoBBYBJ4Dr7XU3A49k+ZweBN7nel0DHAZusY93HnASWG2vPw5cZj9vAs5P9/+T4lg/Bj6atEyBM12vz7PP/yKs/7P3YP0fhoCVdtvmuT7LZa7P/fs5/F/UAL3ASvv1XGCN/fxM4FX2sZqBh4GvJn0ftgCtruv0rN3mSuB3wKeTrvNd9jHX2dfm2uT22vvqwvpu+ew2dAHNrmP/JfDTYmuIFw8TyReee0SkD+vL0oklxrnyiKreq6pR4HvAOek2VNXPAHOwxPEMrH/gTaraoKqN9j6ysQl4nx0VNQAft5fnE8mftKMj53GW3b7tWD7nPVgi/q6kNj2jqnerahj4CtaXeANwIdaX7+9VdVRVDwLfBt7meu/jqnqPqsbUsqlS8VlVHVbV+4AB4C5V7VTVo8DvsYQD4APAF1R1l6pGgM8D57qjeeAfVbVbVV8GHsC+O5sgrwEOqeqdqhpR1eeAnwBvsdeHgdUiUq+qp1U12X7LRCOp79jc3Ap8S1WfUNWoqv4XMIL12UexBHi1iFSo6iFVPZDH8R1iwFoRqVLV46q6A0BV96vq/ao6oqonsK77FUnv/TdV7XBdpydU9TlVHQZ+xth1c/iMqg6o6gvAncBNKdrzTuBe+7sVU9X7gaexvjMOfVifX9lhRL7wvEFV67Air1VYQpwr7a7ng0ClpPCcReRc2x44jRUd7cESnyttoX1jjse7AysSehDYYe8DIJcOPoc59o+K89jlWvdfwGKsL9i+pPcddp6oasw+5jx7+3nuHw4sq6A11Xsz0OF6PpTitdMhvhj4mutYpwDBiv4ckq/LZDrTFwMXJZ3fO4A2e/2bsMTnJRF5SEQuzmPfp4G6HI7/V0nHX4gVve8HPoIVBXeK1RGfyhZKi6oOAH+I9eN5XER+JSKrAGzr5Ye2HdSLdXeZ/P3I9bo5uP8XXsL6H0pmMfCWpHO+FOsuw6EO6w647DAi7xGq+hDwXeBL9qIBXBGyiPixblknsu/nVbURK5Pi7+znO7FslkZV/Wmm97v2E1PVT6vqElVdgCX0R+1HIfh3LKtjo4hcmrRuofNERHxY9tYxrC/ti0k/HHWq6o66Clk69TDwx0nHq1LVx3J4by7tSN7mMPBQ0vFqVfVPAFT1KVV9PZa1dA/wozyOtQ1YkWWbw8Dnko5frap32cf/H1W9FEsYFaufJtfjY+9js6q+CktEd2PdiYF1l6TAOlWtx4qwJdf9pmGh6/kirP+hZA4D30s65xpV/UfXNmcBGfsapitG5L3lq8CrxOpU3IsVmb9aRCqwvN/QJPd/AfCsiAQZi8QSEJEKEanEutYBEam0f2AQkVl2Z5iIyGqs2+e/tyNrp5PzwYk0TETeZbfvZuBDwH9JYjrpBSLyRvtO5SNYlsEW4Emgz+4ArBKr03atWJkjXvBN4JMissZud4OIvCXLexw6gAX2559pm6Wu178EVojIu+xrUyFWp/BZYnVYv0NEGmwbqxfL+nD2M9u21dJxL+Ptj+Tjfxv4gIhcZF/3Gvt/sk5EVoqVhhvC6scYSjr+EvsHOS12tP56sTqLR4B+1z7q7Nc9IjIf+GimfeXIp0Sk2r5+twCpsru+D7xWrDEEfvs7cKWILHBtcwVW53bZYUTeQ2zf8b+xou0e4E+B/8SKlAfIzxZJhZMyuQ7Ynmabb2N9WW/C6gAdwkqbBOtW+V67Lb8G7lDV213vXQg8mqUN3ZKYJ/+XIrII6wfu3arar6r/g+WB/ovrfT/Huq0/bbfnjaoatn3712D53i9idUr+J5BJ3CaMqv4MK1r9oW0hbMfKUsmF32Hd/bSLyMk023wNeLNYmTv/qqp9WJ3Bb8OKOtvt4zs/+O8CDtlt+QCWlYOq7say1g7alsM4W8L273tE5CLX4tuwfmC7ReStqvo08H7g61if/X6sH2LsNvwj1mfejnU34aQA/9j+2yUimfoJfFidmMewrK8rgD+x130GOB8rOeBXQE53nFl4yD6H3wJfsvtgElDVw1gd7H+D1Tl7GOsHxgfx1NN+tVIpyw5RNZOGGFIjIs8D16hqV4H3extWxsc7C7lfA4jIdcCfquobit0WLxGRJdgZWXaH+WT29ROsVOJ7C9G2UsMMJDGkRVXPLXYbDPlhR7LjollDelT1TcVug5cYkTcYDHkjIv1pVt2gqr+f0sYYMmLsGoPBYChjTMerwWAwlDFG5A0Gg6GMKSlPfs6cObpkyZJiN8NgMBimFc8888xJVU05uLKkRH7JkiU8/fTTxW6GwWAwTCtE5KV064xdYzAYDGWMEXmDwWAoY4zIGwwGQxljRN5gMBjKGM9FXkT+Qqyp7LaLyF12RUSDwWAwTAGeirxdTvRDwHpVXYs13djbMr/LYDAYDIViKuyaAFBl1w2vJnVRf4PBYDB4gKcib8/T+CXgZawJintS1XueCq7/6sP84Im0qaRp+dW243zwruc8aJHBYDB4j9d2TRNWsf4zsOZerBGRdyZtc6uIPC0iT584ccKztuxu7+Nvf5ZuXo30PHXoFPftaM++ocFgMJQgXts112LN13nCns7sp8Ar3Ruo6u2qul5V1zc3T2jKU0+JxpTRaAxTrdNgMExHvBb5l4EN9hyMAlwD7PL4mOOIxiYu0JGYogrhqBF5g8Ew/fDak38CuBtrHtIX7OPdnvFNHhCJxbJvlO69Ueu9o9GJ78NgMBiKhecFylT108CnvT5OJiKTiMKdu4CRcJTaUEnVczMYDIaszIgRr5FJ2jVgInmDwTA9mRkiPwmBHovkjcgbDIbpx4wQeUeo/T7J+72On28ieYPBMB2ZESLvWC5+yV/kTSRvMBimMzND5O2OV98EznbMk48WskkGg8EwJcwMkbctl0lF8hETyRsMhunHDBH5SXjyUSPyBoNh+jIzRD46cZF3IvlRI/IGg2EaMjNE3rFrJpFdYyJ5g8EwHZkhIm93vE7CkzeRvMFgmI7MDJGfhF0TjnvyJrvGYDBMP2aGyNuWi4nkDQbDTGNmiPwkInnjyRsMhunMjBD5yZQ1MJG8wWCYzswIkQ9HHbsm//dGYsaTNxgM05cZIfImkjcYDDOVGSHyk0mhjBiRNxgM05gZIvITHwxlatcYDIbpzMwQ+clk1zhzvBqRNxgM0xBPRV5EVorI865Hr4h8xMtjpqIQI15NJG8wGKYjns5Mrap7gHMBRMQPHAV+5uUxUzGpKpRG5A0GwzRmKu2aa4ADqvrSFB4TGLNcJufJmxRKg8Ew/ZhKkX8bcJfXBxkYifC5X+1kODwmytE00//d/vABdrf3pt2Xqk55ds1vd3Vwz3NHp+RYhsnzk2eO8PPnzfUylC6e2jUOIhIEXgd8MsW6W4FbARYtWjTpY33zoQN8+/cv0lJXyfsvXwqMFRlzR/Kqyufv3U3vUIRVbfUp9+X8OMDU2TX/9rv99A6HecN586fkeIaJo6p84de76B+JcNEZs2lrqCx2kwyGcUxVJH8D8KyqdiSvUNXbVXW9qq5vbm6e9IEcQR+Njoly1ClQ5hJ5J0KPqpKOiEvkpyKSV1X2d/Zzsm/E82MZJs/+zn5O9o8yHI7xlfv3FLs5BkNKpkrkb2IKrBqAVAk0jvC7LXknSo/F0ot8YiTvvSd/vGeY/pEIvcMR0wcwDdhysAuAjWta+fEzRzJafwZDsfBc5EWkBngV8FOvj5WOaAohd6L0SAaRT4jko95H8ns7+uLPu/pHPT+eYXI8frCLeQ2VfPFNZ1MXCvCFe3cXu0kGwzg8F3lVHVDV2ara4/WxAJxgXV02TNi2a9zOjJNxk+oHwCE6xXbNvo7++PMTxrIpaVSVLQdPsWHpbBqrg3zw6uU8tPcEj+w7WeymGQwJlN2IV8eucQt61LZrYq6FcU8+YyQ/lno5FR2v+zrHIvmT/UbkS5l9nf2cGhhlw7LZALz7lYtZ0FTF5+/dldECNBimmvITecab8qksmWgOHa/ONtUV/imJ5Pd29LN4djVgIvlS5/EDlh9/8VJL5EMBPx/duJKdx3v5mUmBNZQQZSfyDm7pdiLylJF8NEMkb6+rDvk9j+SdzBpHNEwkX9psOdjF/MYqFjRVxZe99ux5nL2ggS/dtydhnIbBUEzKTuRT2jW2oKfy5DN1vMYj+WCAaEzj7/ECJ7NmzfwG6isDnDQdryVLLKY88aLlx4srncvnE/7mxrM43jPMdx55sYgtNBjGKD+RT7HMSaFMEPnYeJ8+mUhc5P2Atxk2TmbNipZa5tSFjF1Twuzt7LP8+KWzxq3bsHQ2157Vwn88eIAuczdmKAHKTuQd1GXYRFMIejSHFMpossh7aNk4mTXLW+torg1xwghEybLF9uM32NZaMp+4YRVD4Sj/+tt9U9ksgyEl5Sfy9u2zO0B35nh1y7mzLFMmhOPlVwet6g9e+vL7OvuYUxtkVk2QOXUh48mXMFsOnmJBUxULZ1WnXH9mSx1vu3AhP3jiZQ6e6E+5jcEwVZSdyKeya+Jpkil8ekfIU+F0vNaEvI/k93b0s7ylDsCK5I1dU5LEYsqWF7vSRvEOH7l2BaGAj3/aZModGIpL2Ym8Q2LUnilPPv0+nG2qKpxI3puMCSezZnlrLQDNdSH6hiMmQ6ME2dPRR/dgOJ4FlY7muhB/fMUyNu1o5+lDp6aodQbDeMpO5H0p0mucAmVu4Y/nyWeI5J1tnEjeK7vGyaxZ3mpF8nNqg4BJoyxFnPz4i1J0uibzvsvOoKUuxOfv3ZUwAttgmErKTuQdjY9lyaRxPPkMafJxK6fK445XJ7NmeYsVyc+pDQGYNMoSZMvBLhbOqmJBU2o/3k11MMBfXbeCZ1/u5tfb26egdQbDeMpP5FMsi6RIocwrkve443V/p9U5t8KO5JvrbJE3vnxJ4eTHZ7Nq3Lz5goWsbK3ji5t2m8ngDUWh7ETewZ1CGUlh1+RWu2ZqUij3doxl1sBYJG/SKEuLXe299AyFs3a6uvH7hE/cuIqXugb5/pYpn/nSYCg/kU814jUSH/HqEv5odpF3Sh54nUK5t6OfM22rBmC248mbSL6k2HLQ6kDNR+QBrlzRzCVnzuZff7ePnqGwF00zGNJShiI/3rBJVdbAsWmKHck7mTWOVQNWsauGqgrT8VpibDnYxeLZ1cxrrMq+sQsR4ZM3nEXPUJh/f3C/R60zGFJTdiLvkHsKZe4jXr1IoUzOrHGYUxs0dk0JEY0pTxzsYsMZ+UXxDmvnN/AH587nzkcPceT0YIFbZzCkp3xFPkXUnqrjNfPMUIkjXr2I5JMzaxya60Kc7DPZNaXCruO99A5H2LAse+pkOv5q40oAvnzf3kI1y2DIStmJfKo5XuPZNa5l4Vw8eSeS9zBPPjmzxmGOqV9TUjjzuebrx7uZ31jFey89g589d5TtR6dkojSDofxE3iExu2Z8x2s+nnyNx5G8O7PGYU5tyHS8lhBbDnaxZHY1cxvy8+OT+ZMrlzGrJmgGSBmmjLIT+fjMUClqx6fKuMlpZigPPfl9nYmZNQ7NdSH6Rkxpg1Ig6uTHL5t4FO9QX1nBh64+k8cOdPHgnhMFaJ3BkBnPRV5EGkXkbhHZLSK7RORib49n/U2VE5+q1HDGSN7+cQhV+BApfCSvquzv6B9n1YBVpAzMNIClwK7jvfQNRyZl1bh5+0WLWTK7ms/fu8vTiWgMBpiaSP5rwCZVXQWcA+zy8mApR7zGJubJO++r8PkI+n2MFPgLebxnmL4UmTUAc+pM/ZpS4fEs9ePzJRjw8fHrV7Gvs58fP3OkIPs0GNLhqciLSANwOfAdAFUdVdVuL461v7Mv3olpH4sXTw5wxyMvcmpgNL7MIRdP3lnn9wuhgI+RsPWe4XCUB3Z3pnzP/Ts7co7499ntTc6sAWiurQQy16959uXTHO0eyulYudAzGOaBPanPq9CcGhjlsf0nPdn3Ewe7OFbAz2XLwS6Wzqmhtb6yYPu8fm0bFyxu4iv372VgJFKw/RoMyXgdyZ8BnADuFJHnROQ/RaTGvYGI3CoiT4vI0ydOTNyjvPYrD3PtVx5KGPH61d/s5e9/uTO+TUpPPodIPuATggF/fPq/e547yi3ffYrjPYlCsr+zn/f/99Pc+8LxnNp8+JSVL71kds24dU4kn86uicWUm+94kn8r4OxD33r4ALfc+RQvdQ0UbJ+ZjvXO7zxB33BhR4CGozFuvvMpPnr31oLsLxpTnnzxFBcVKIp3ELHmgz3RN8I3HjADpAze4bXIB4Dzgf9Q1fOAAeAT7g1U9XZVXa+q65ubmyd9QKfjVYGBkSgLmqrY+unreO058xJ9+jxSKP2+xEj+WM8wAN2DiQJ1etCKul/qym2wi9Op6pQydjO7xqlEmVrkD3UN0DscKaid84gdWU9Fh+Cu433E1PpbSPZ29DEUjvLo/q6CpCnuONZD30gk5Xyuk+WCxU288fz5fPv3BxPuQg2GQuK1yB8BjqjqE/bru7FE3zPckfxoNMbs2hANVRX4JM2I10wTeUedSN5HKOCLR/JOdN2fdJvtRKW5jmgcGrVEvrJivMgHAz4aqyvSRvIv2AJ2erAwkXDPYDi+z6mwbPa09wKw81hh88W3Hrb2F/T7+NbDBye9Pyc/Pp/Kk/nwyRvOorLCz6d/sd2kVBo8wVORV9V24LCIrLQXXQPszPCWwh0bZTQSJeS3TlFIU7smQ0F5ZxufWKI7YkfeJ/qsSH68yFuvD+cq8uEoFX6hwp/6MsypTT/X6wtHHJEvzKjYxw+eRBXOXtDA4we64j9AXtA9OEpHr3VeO471FnTfWw9301Rdwc2XLOHeF47HLbGJsuXgKZY219BSQD/eTXNdiI9uXMmj+7v4VY42n8GQD1ORXfNB4Acisg04F/i8lwdzFygLR5WKgPXaJ5JygFTGSD6mBHyCiKSO5IdTi/yR07l1+g2Fo1QGxkfxDs2ZRN6OunsKFMk/ur+LmqCfj1y7nJFILB7BesGedsuiqarwF17kj3Rz9oJGbrlkCT6B7zzy4oT3FYnGePLFUwXLqknHOy5azJp59Xz2lzvHBQ4Gw2TxXORV9Xnbcz9bVd+gqqe9PqZ1XCuvPehEyQLu+UEcKyZT7ZpoTPH7rB+JoMuT77RFPjkrwhH54z3DOeU/D4djVAbTi/ycutQTesdiGhfH7qFwQW7zH91/klecMYtXLptDVYXfU8vGqddz/do29nX2FWz8weBohL0dfZyzoIG5DVW87pz5/O9Thzk9MLG7nR3HeukfiXhm1Tj4fcJn37CWjt4RvvYbU9fGUFjKcMTrGKORGMGAY9ckZtDnNBjKjuTBKv87Go0Ri2k8uk7nyUdjynG7czYTw+EoVSn8eIc5tcGUKZSHugboH4mworWWaEzpHZ5c9Hese4iDJwe45Mw5VFb4ueTM2fxud6dnHvHu9j7qKwNcvaqFcFTjoj9ZdhzrJaZwzsJGAG69fClD4eiEJ+t4/GDu87lOlvMXNfG2Cxdyx6OH4nc6BkMhKD+Rj3e8KqPRGEHbDhnf8TqWJ59OzKIxJWDfCQQDPkYiUbqHwvGBVMki736di2UzNBqlsiL9JWiuC9E/EhnnjztWzWXLrWykyVo2j9pZNZecOQeAK1e2cOT0EAdOeJNKubejj1Vt9ayZVw/AzuOFsWy2Hu4G4OwFjQCsbKvjqpXNfPexQxMqD7HlYBfLmmtoqfPGj0/mY9evoq4ywKd+bjphDYWj/ETe9Xw0EqPCby0RScqTd3W4pgvmI7GYK5L3MRqJJdgnqewaZ/tcMmyGI9ki+dRplC8c6SEY8HHhEivCnGzn62MHuphTG2SlPfL2ypXWj8eDHlg2qsru9j5WtNWyZHYN1UE/Owvky2890sP8xqr4HLkAt16+jK6BUX7ybH4jSyPRGE8VqF5NrsyqCfKxjat48sVT3PP80Sk7rqG8KTuRd1CsFMqQbdek63iF9JZNJJroyY9GYnT2jdkwqeyaM+bUIAKHc47kM3e8wvi5Xl842sNZc+tptgdMTUbkVZVH9p/k4mVz8NnnuqCpmhWttZ748u29w/QNR1jZVo/PJ5w1t54dBUqj3Hq4m7MXNCQs27B0FucsaODbDx/MaM0l88LRHgZGo553uibztgsXcs7CRj73q91mqkBDQSg7kXeya5I7XkUSI/ZoLiKf4Mn7GHFF8gGf0D+SaAH0Dkdoqgkyt74yt0g+HKUqQ8erE5G67x6cTtd18+tprLZEPnlQVj7s7+znRN8Il56ZKGZXrWrhyRdPFTzbY7ftNzt3DWvm1bPzWC+xPAQ4FacHRnn51GDcj3cQEW69fBmHuga5f2d7zvub6Hyuk8XnE/7h9WvpGhjhX+43nbCGyVOGIj/23N3xCpKyrAGkT6OMxhS/PzmStwR30axq+pOG5PcPR6ivDLCgqTo3Tz5LCmUqu8bpdD17fiNNcZGfeCTvjHJ95bI5CcuvWml1ij6yr7D1ZfamEPmB0SgvTTKffeuRboBxkTxYWTyLZlXzzYcO5ux1bznYxfKW2vg1mErWLWjgHRct4r8fP1SwuxzDzKXsRN5BUcLRWHygkRWQu+waV4pjugFRViRvvT8U8Mcj+aoKPy31IQaSIvm+kTB1lRUsaKriSA6iNRyOZYzkZ9falShd0wA6na5r5zdQX2lNZjKZUa+P7rcmp144qzph+QWLm6gLBQruy+9p76OtvpKG6goA1syzRHmyYrbtSA8isG7+eJH3+4T3X3YGzx/u5qlD2TN4w9EYTx3yPj8+Ex+9bhVN1UE+dc/2Sd/lGGY2ZSfyTqAWjVkiHU+hTLJrcovkYyk8+RFa6kPUhiroS9HxWlcZYMGsatp7h7Pmfw+FM3vyFX4fTdUVnOgf6wdwOl2Xt9YS8PuorwxMOJKPRK1BT8lRvHPsy1bM4YE9hU2l3N3ex8q2sdLKy1trCfhk0p2vWw93s6y5lrrKipTr33zBQmbVBLn94QNZ9/XC0R4GR6NT2umaTEN1BZ+4YRXPvtzN3aYcsWESlKHIW4LkCKw7T15TTBoCY+mUyUSiY5580G+NeO3oHaa5NkRtyJ+QXaOq9A1HqA0FWNBURUyhPUuu/HCWFEpwpgFMjOTPmlsfv0NpqglOOJLfeqSH/pEIl545XuTBSqXs6B0pWBGxSDTG/hP9CSIfCvg5s6V2UiNfVZWtR3o4x06dTEVV0M+7L17Mb3Z1si9LXr4z2vcVZ3ifH5+JN52/gPWLm/jHTbsnZckZZjblJ/L2X6cEQdBl17gj+bDLrkmj8QkjXkO2GB89PWRF8pWBhE7J4XCMaEzjdg1kr2GTLYUSrM5Xx5N3Ol3PdlkSjdVBuieYheHUc08XsV65wkqlLFSWzaGuQUYjsbgf77BmXsOkRP5YzzAn+0c4Z+F4q8bNuy9eQmWFj2//PnPhsscPdLGitTh+vBufT/j716+le3CUf968p6htMUxfyk7kHZxiYmN2zQQi+VhiJA9WCmBLXSU1oUSRd0a71lUGWNhk+duZMmzC0RjhqGYV+Tm1oXgKpdPp6vadG6sqJhzlPbL/JGvm1Y+bRNyhpb6StfPrC+bLOyNb3ZE8WJ2vJ/tH6OzNPko4Fc4gqEyRPFh56G+5YCH3PHeMjjTHCkdjPH3otOelDHJl9bx63vPKJfzPky/Hz9NgyIesIi8iH7b/XuJ9cyaPo+MjkcRIXiT1vK+QayTvjy9rrgtRFwowGonFbSGntEBdZYC5DZX4fZIxw8YZgZmp4xUcu8YSeXenq0NTdcWE8uQHRyM893J3fJRrOq5a2cIzL50uSCG03e19+IRxE5c7I18nGs1vPdJNhV9YNXf8NIrJvO+yM4jEYtz56KGU67cd6WEoPPX58Zn4i1etYE5tiE/9fHteuf4GA+QWyd9i//03LxtSKJyvwEgkKZJPSqHMLZKPxcsahFzlgJvrQtSErMwWx5d3R/IBv4+2+sqMZW6HbJEP5WDXDIxGGRyNJHS6OjRWB+keyF+Anzp0mtFoLKvIX7myhZjCw/smP5HI3vY+lsyuGdfZvHqS5Q22Hu5m9dx6QhnSUR0Wz67hhrVz+cETL6UcA7AlXq+mdES+vrKC//fqs9h2pIcfPvVysZtjmGbkIvK7RGQfsFJEtrkeL9jlg0uKtB2vkjjHa0IKZZroKOoeDFWRKPK1tsg7QuH8dbI7Fs6qyhjJOxUts9s1Y2mULxztYbWr0xWgqTpI30gkoY8hFx7bf5IKv3DhkqaM2527sJHG6oqC+PJ7OvrGWTVgfWaLZ1dPKI0yGlO2H+2N16vJhVsvX0rfcIQfPjleMLcc7GJVW11aC6tYvO6ceWxYOot/2rSHLjO5uyEPsoq8qt4EXAbsB17rerzG/luSJNs1yR2vuaRQRtylhl3C2pJC5Ptcdg2QdUCUE8lnFXl71Gtn37A90jWxc7HRzjfPdwj8I/tPcv6iJqqDgYzb+X3CFSuaeWjPiUnlaw+HoxzqGmBFa2pLZfXc+gnZNQdP9NM/Ehk30jUT5yxsZMPSWXznkRcTfhxHI5YfX0pWjYOI8NnXr2VgJMIXN+0udnMM04icOl5VtV1Vz1HVl5IfXjcwXxy9diL5CnfHK4kdr87o2EiawVAZI/nKdHaNJboLmqro6BuO20bJjE39l/kSOPVrnjp0elynK4yJfD6dr6cGRtl5vDerVeNw9aoWugZG430CE2FfRz+qsCpFJA+WL/9S1yC9eU7svdWeIeucFCNdM/HHly/jeM8w/7f1WHzZtiPdth9f3NTJdCxvreO9l57Bj54+wjMvTcm0DIYyIJeO1xeSbJrStmtsIXdSKEPujtekSN4pXhZLE8mHo4rfHvEa9I+VLJ5dM+bJ96WJ5Bc2VaMKx7pTZ3EM5xjJO/VrHLtkbZLIN02gfs3jB7pQJWeRv3x5MyKTS6XcY2fWrEgr8tZ57cozmt96uJvaUIClzbXZN3Zx5cpmVrbWcfvDY6UO4n78GaUXyTt86JrltNVX8ql7tuc0MY3BkEsk79gyyY+StGuc/3vH807X8RqJxuIddelmh4q6Sg07+5ldG8Lvk7hdM5Ak8jVBx66xcuXTpVE6dk2mmaHASvsTgWdeOk0oqdMVxiL5fAZEPXrgJLWhQM7Rb1NNkPMWNvLAnol3vu5p7yUY8LFkdk3K9RPNsNl2pJu18+vjtlquiAjvv3wpu9v7eGivdV5bDp5iVVsdTSXmx7upCQX41GtWs/N474QnQzHMLHLx5MdZNKVs1ziTb4/LrhHGlRqOR/IZqlA6BcqcbVvsyDruyQ+PiXxtKBAXmwV2LZjDp1L78rlG8lZpgyDRmCaMdHVwIvl80igf3X+SDUtnxTOHcuGqlS1sO9Kdds7ZbOzp6Gd5S21aMW6pr2RObSivDJuRSJSdx3vz8uPdvO6cebTVV3L7wwcZiUR5+qWprR8/UW5c18Zly+fw5fv2ppwe0mBwk/O3XEQ2iMhTItIvIqMiEhWRrN9IETlkWzvPi8jTk2tudpyoPO7Jp+l4jbrq2qSP5HVcJO/YJzXjOl7DcasGoK2+koBPskfyWUQexjJsUhXfyteTP3xqkJe6BnO2ahyuWtWCKjy8d2LR/J723pSZNW7WzMuv83X38T7CUc06CCodwYCPP7p0CY8d6OIHW15mOBwryU7XZESEz7xuDcORKF+4d1exm2MocfIZ8fp14CZgH1AFvA/4Ro7vvUpVz1XV9Xm2L2+cdMh4WYM0tWvC0RwiedekIWkjeVcKpVvk/T5hXmP6NMrhHFMoYeyHJZXI14YCBHySsyf/2IHEqf5yZfXceprrQvxud/6+fPfgKB29I+PKGYw7xrx69nX0pe2sTsYpLzzRSB7gplcsoi4U4B837UYELipyvZpcWdpcy62XL+Wnzx3lCbsvwWBIRV5lDVR1P+BX1aiq3glc702zJo4TlTvzsLpTKBUrq2VgJEI0lujJO8vdpIrknfk+/T6hqsKf4Mk7wu+woKkqbf0aJ7smF5F3aqgkd7qCFdU1Vlfk7Mk/ur+L5roQy1vy66j0+YQrVzTz8N4TeXf4ORNT5xLJR2LKvo7+nPa79XAPc2qDzGuY+BysdZUVvH3DIkYjMc5qG5uIZTrw51ctZ35jFZ/6+fa8x0kYSov9nX1pS21MlnxEflBEgsDzIvJPIvIXOb5fgftE5BkRuXVCrcyD5IFN8UlDxOp4/dTPt/OnP3jW8uTt9MWoKh//yTY+dNdzCe+18uStbeoqKwj4hEWuuuvuImWWXZNY5nZ+YxXHulNH8mMjXrN/hAubqqkLBcZ1ujo0VgdztmuefPEUFy+dHZ9BKx+uWtVC73Ak7/S9dDVrksm3tvzWI92cs6BxQufi5o8uOYNgwMely/O7uyk2VUE/f/fa1ezt6OcXzx/L/gZDyfK5X+3ird963JMJ3PMR+XfZ2/85MAAsBN6Uw/suVdXzgRuAPxORy90rReRWEXlaRJ4+cWLyQ+eTrRefLQCODHT2jXC0e4ioq+M1GlXae4bHzU7kzq5pqKrg1x++jD84f358fW0oEJ8C0Kkl76a+qiLeMZvMSDiKyJgNlIkPXLmM//vgpeM6XR1yrV8zHI7S3jvMsjzTDR0uX9FMMODjvp0deb1vd3sf9ZUB2uozR9yLZ1VTGwrkVFu+bzjMgRP9k7JqHFrrK/n1hy/jw9csn/S+pprrVrcyv7GKX24zIj9d6R0O8+j+LjauaZt0wJKKnEXezqYZVtVeVf2Mqv6lbd9ke99R+28n8DPgFUnrb1fV9aq6vrm5Od/2jyPZXneSORyxj8ZiDI5EiEQ1btdEVRmJRMeNGo3ElIB/7ENf3lqXILQ1IX98CsC+kci4SL4m6GcwHE356zwUtsoM53JRa0MBlsxJnXoITiSf3a45bte3n2+nd+ZLbSjApWfOYdP29rwijr12OYNs52pN7F2XU+frC0d7UE093d9EWNZcG+9Mn06ICDeua+OR/SfNxN/TlAd2dzIajbFxTasn+88nu+YSEblfRPaKyEHnkeU9NSJS5zwHrgO2T67JmVHSRPKu0a0Do1EisdhYJB9TRiKxcV8StyefitpQID4FYHJ2DUBVMIDqWCerG0fkC4FVbjj7F9yxjuY1TtzD3rimlaPdQzlnwajquNmgMrF6bj27jmef2HvrYWeka2NO+y1nblw3l3BUuT/POyxDaXDfjg6a60KctzBzHamJko9d8x3gK8ClwIWuRyZagUdEZCvwJPArVd00kYbmSnKA6bNF2tHqaEwZGIkQUxJSKJ2ywU7+urPc8eRTUWvXlA9HYwyHY9QlRYI1IUvEB0bHWzZDo7Gc0idzwZodKrtdc9TO9FnQWJ1ly/Rce1YrPoH7drTntH177zB9w5GsmTUOa+Y1MDBq1bnJxLYj3SyaVV3SA5eminMXNjK/sYp7Xzhe7KYY8mQ4HOWBPZ1sXNMa16pCk4/I96jqr1W1U1W7nEemN6jqQbvmzTmqukZVPzfJ9mYl2UZwPjfHKojENJ6B49g1MTuSh8RCX7lE8v0jkXElDRyc4l+DI+NTAocj2af+y5XG6gpGkn6gUnGkewgRaJtENsrs2hAXLpnF5h25RY2745k19TltvzrHka9bD3cXxI8vB0SEG9a28ft9J/Ku/WMoLr/fd5LB0Sgb17R5doxcatecLyLnAw+IyD+LyMXOMnt5STHek08UaXfteCezJRLTeG62I/KqmjBpSCpqQgEGRiLxztVUnjykjuSHR6NZJwzJlVxHvR49PURrXeVYxtEE2bimjT0dfbx4MnO0Da70yRwj+RWtdVT4JaPId/YNc6xnOO+iZOXMjWdbls1vjGUzrdi0vZ36yoCng/By+bZ/2X5cBKwHPu9a9iXPWjZBkouNOSLv/HVXnHQPhkqO5J1oP2MkXxmgbyQSj55qkyN5274ZHB0fYQ+Fo1TmMMlFLjRW2fVrskwecrR7cMKdrm6uszuINudg2ext76OtvpKG6oqs24JloS1vqctY3mCb48ebSD7OuQsamdtQaSybaUQ4GuO3uzu4dnVr2sy5QpBL7ZqrMjyudrYTkfd41so8SI7kRRL/un8E3J58XOTtDkwn397vzyDyQWsKQCeCHm/XWCI+mCqSDxcukm+MV6LMHMkf6x5mXuPkRX5BUzVr59fnJPK72/vSVp5Mx5p59ew81pM2g2fbkW58MlbUzGD1Pd2wdi4P7z1pLJtpwpMvnqJ7MMz1Hlo1UNiJvD9cwH1Ngsx58omRvDNv69hcrflE8k7KnZOaWJ9k1zgiP5DCkx8KF7Lj1a5fkyGFLhZTjvcMMb8AIg9w/Zo2nnu5O+MovUg0xv4T/WlryKdj9bx6TvaP0pmm+NbzR3pY0VqXdcKTmcarz25jNBrjt7uMZTMd2LS9naoKP5evmHzqeCYKKfLedA3nSfJ0rcl58u5iZI5dM+TqsHREPmr/GGTMrrEj93Zb5JMjeafscNpIvmAplNk9+c6+EcJRLYhdA8Q7ijINjDrUNchoJJZ2Nqh0ZBr5qqpss0e6GhI5b2ETbfWV/GpbbplPhuIRiymbd7Rz5crmggV76SikyJfENPLpPHlxpVA6OCLv9szHInnr1yJbdg2MRfLJtWuq4ymUKSL50cJm10DmiUOOdlujeRcUKJI/s6WWpXNq2Lw9vaA45QzyjeTPmmttv+PoeF/+5VODdA+GjR+fAp9PuGFdGw/vOxGfqcxQmjx/pJvOvhGuX+utVQPlGMmn9eSdSN6dXeN45ikieceTz0nkrfzz8dk1Tgplikg+UrhIvrLCT1WFn9MD6SP5o92TG+2ajIhw3Zo2thzsivdjJLO7vQ+fWD8I+VBXWcGS2dUpO1+d6f4KNdK13Hj1urmMRmITqhZqmDo2b2+nwi9ctarF82MVUuQfLeC+Joy7s05kTNwdqU4dyY+JcG+SJ1+RoePV8eTbe4YJBXzjUhOrUvyIOAyNRrPOCpUPTdUVGT15ZyBUITpeHTauaSUSU367O7Vls7e9jyWzayZ0O7pmXkPKNMqth7sJBXw5j6CdaZy/qInW+hC/2maybEoVVWXTjnZeuWzOuH48L8inrEGriHxHRH5tv14tIu911qvqn3vRwHxxB/LuHPl4WYMc7ZqxSD79R+R48Md7hsdF8WDdPldV+Md58k7KZqFSKAEaslSiPNo9SENVxThLaTKcs6CR1vpQ2iybPR19efvxDqvn1fPyqfETe2870s2aeeNnyDJYOFk2D+49Ea+Qaigtdrf38VLX4JRYNZBfJP9dYDMwz369F/hIgdszadyevNtpiRcoS5EnP5TSk889u6ZnaHzdmrFt/OM8eSdds1AplOBUoswcyRcqs8bB5xM2rmnjob0nEj5DsDqWD3UNTDjidka+uitSRqIxXjjaY/z4LNxoWzYmy6Y02byjHRF41WpvCpIlk4/Iz1HVHwExAFWNALlN4TOFuD15yRLJO9GgI8KhgM8VyVtCnIsnD+Mzaxyqg4FxnvxQjvO75kNTdeb6NUe7hwrmx7vZuKaN4XAsPhm2w76OflSz15BPR6qJvfd29DMcjpnMmiysX9xES13IDIwqUTZtb+fCJbPikwF5TT4iPyAis7EdERHZAOQ2u8MUki6Sl3ip4bH1Ab+PgE8Ysu2U5rpQfpG8KxJPL/LjI3kvRL6xuiJtB6iqehLJA7zijFk0VFWMK1i2J8eJQtLRUldJc10oIZLfVoDp/mYClmXTxoN7Toyb7cxQXA6dHGB3e5+ntWqSyUfk/wr4BbBMRB4F/hv4oCetmgSaIPKuSN7+686uCfgEn0/innyLW+Sj2bNrAn5fPA2yLpS6A6UmFEhpZUBus0LlSqPd8ZpqlGjvUISB0SgLPIjkK/w+rjmrhd/s6kiYgm5Pey/BgI/FsyZe8dKa2Hssjth6pJv6ygBLZk98nzOFG9fNZcRk2ZQcTv+VV7XjU5HPpCHPAFcArwT+GFijqtu8athEcWtcqo5Xt53j9wmBBJGvjFdzjEfyGbJrAGptcU+uW+NgRfJJdk0e87vmSlN1kGhM6U0xE9URO0e+kJk1bjauaaN3OMITB0/Fl+3p6Gd5Sy2BSXSQrplXz/7O/njxuK2HLT/ei9lzyo31S2bRbCybkmPzjnbWzW9gQdPUBSr5ZNdsAz4GDKvqdlUtydEWmuDJjz1PrkYJVnqkXySe/dJSb3lkvUNhlyef+SOqtQc8ZbJrkksNO5F8ITtenfo1qSwbJ33SC7sG4PLlzVRW+BKybPa09+ZceTIdq+c2EIkpe9v7GRqNsqejz+TH54jfJ1y/po0H9nSmHHFtmHo6eod59uXuKcuqccgnzHotEAF+JCJPichfi8gij9o1YWJZ7Bo3fp8Pv38skm+2O0J6hsJxuyaTJw9jEXyqFEqwBkSNi+RtkS/kcOYme9Rrqs7Xo/aMUF50vIL1Y3XlihY272gnFlO6B0fp6B2ZdC77WOdrDzuP9xCNqel0zYMb181lOGwsm1LhviJYNZD/HK//pKoXAG8HzgZe9KxlEyTZjnFIFckHbLvGsU+cSL5nKJzTiFcYG9Vany6SD/nHDYZypgMsdMcrpBb5Y91DVFb4mO3hLEob17bS2TfC80e64zXk860+mcwie2LvHcd6ed6UF86bV5xhZXAYy6Y02LSjnWXNNZzZMrUD+fIaGSMii4E/tB9RLPumpNA02TWpQvmAX/CJxP33ljprxqSeoXA8vTJbJO/YNOkGGdUEA+MyHLyI5MfKDaewa7qHmNdY5amXffXKVgI+YfOO9nh9nHxr1iTj8wmr59az83gvvcNh2uoraa2f+KxWMw2/T7h+bSt3P3OEwdGIqdpZRLoHR9ly8BQfuGLplB87H0/+CeBngB94i6q+QlW/7FnLJkgsoaxBZrvGieQdmusmEMmHMts1VUE/I5FYQurm8GjhPfmmDDXlvUqfdNNQXcHFy2Zz344Odrf3UVcZoK0Agrx6njWx93Mvdxs/fgI4ls2De05k39jgGb/Z1Uk0plOaOumQjyf/blU9X1W/oKoH8zmIiPhF5DkR+WWe7cubxLIG7ufjxdrv8yVMntviEvmx2jWZP6IxkU8fyUNifZxhO1ukcpLT8Llx7KJUo16Pdnsv8gDXrWnjxZMD3Lezg1VtdQW5c1g9r57B0Sgvnxo0Vs0EuOiM2cypDfIrY9kUlU3b25nXUMm6+VMfqOSjMu0i8hURedp+fFlEcm3xh4FdE2hf3sSypFC6cUfyPoEm27PuSciuyWLXZBF5p9yw25cf8iCSD/h91FcGxkXyw+EoJ/tHp0TkN65uRQRO9I1MuGZNMu7Zn0yna/747dITv9vVOW68hmFqGBiJ8Pt9J9i4tq0o6b/5iPwdQB/wVvvRC9yZ7U0isgB4NfCfE2lgvqQbDOV+7gh3wC/xSD4Y8FHh91ET9CdE8tk8+VwjebcvH/fkC1igDKwfqeRI/pjHmTVuWuorOc+Otifrxzssb6mLVwJdZ+yaCXHjurkMhaM8uMdk2RSDh/aeYCQS83yav3TkI/LLVPXTqnrQfnwGyKUX4atYHbSxLNsVhFhSqeFUzxvsia8r7LIGMDYVYENVRV6evGOT1Fel9uTH5nl1RfLhKMFAolVUCBqrg+PKDcfTJ6cgkoexGaMKFckHAz5WtNaxdE5N/LoZ8uOiM2Yxq8ZYNsVi0/Z2ZtcEWb9kVlGOn093+5CIXKqqjwCIyCXAUKY3iMhrgE5VfUZErkyzza3ArQCLFk0+7d49/V8qHx7gdefMY/W8eubUhuLbOBUpG6qD9Ay68+Qz/w6+7tz5VIcC8cycZKrjnvyYyI+EYwVNn3RorKoYl0LpRR35TLxjw2KCAV9B/6Fve92ahLl5DfkR8PvYuKaNnz9/lOFw1PPp5gxjjESi/G53J685e27WgNEr8onk/wT4hogcEpGXgK9jlTfIxCXA60TkEPBD4GoR+b57A1W9XVXXq+r65ubJT2irpE6hdAv+nNogb12/EBgrW+DUkXEm34hH8lnKGsyqGdtXKsamAHTZNaOFmxXKjVVuOEnku4fwCbQ1TE3qYW0owC2XnFHQf+gLl8zi4mWzC7a/mcir181lcNRYNlPNYwe66B+JsHGKR7m6yWcw1POqeg7WIKh1qnpetto1qvpJVV2gqkuAtwG/U9V3TqrFWcil49Vtk/jtFUG/I/JWyd5wDnO85sLYFICJdk0hO10dGquDdA8k2TWnh2irrzSTbMxwNiy1LJt7XzCTfE8lm7e3UxcK8MoiBin55MnPFpF/BR4EHhCRr9mlh0uK5On/4s/J3Akb9+Ttkr25evLZcDx5dyQ/HI7G7aFC0lQdpG8kklAN0qs68obphWXZtPLbXR3x2kkGb4nGlPt2dnDVqpa4vhSDfJTmh8AJ4E3Am+3n/5vrm1X1QVV9TX7Ny590kbxbq/2pRD7JrgnnWLsmG072zWBSdo03kbzVMdnj6nydqhx5Q+lzw9q5DIxGx03wYvCGpw6d4tTA6JQXJEsmH5Gfq6qfVdUX7cc/AFNbaScH0hYoS2fX+BI7Xp2SvT22t124SH4sehoOe+PJOyLv5MpHY0p7z/CUdboaSpuLl82msbrC1LKZIjbvaCcU8HHFisn3NU6GfET+PhF5m4j47MdbseZ8LSnSlRqWdFF9PE9+LIUS4ES/JZTZsmuyEQr48EniPLJDHol8U1L9mo7eYSIxNXaNAbBShjeubuO3uzqNZeMxqsrm7e1cvqI5fjdfLLIqmIj0iUgv8H7gf4AR+/FD7NTHUiKxrEHq2jX+hEje+gjckTxAV//IuG0ngoiMKzc8HI55ksY2VonSEvmpzpE3lD43nj2X/pEIDxvLxlNeONrDsZ7hotSqSSbrT4yqxke1iMgsYDlQsqUAE0a8un7CJN3oV/upI/KOUJ60RX6ynjzY5YZHEssaeCHyzg+Uk0bpjHb1Yto/w/TklbZl8+vt7VxXAgJUrmza3o7fJ1x7Vkuxm5L7YCgReR9WDZoFwPPABuAx4BpPWjZB0nny6XLmxyJ5S3Sdkr0n+0fxCQUZlTo+ko9SFSx8dk2yJ39kigdCGUqfCr+P61a38usX2hmJRIua9VHObN7RzsVLZ8f1pJjkozQfBi4EXlLVq4DzgJ7Mb5l63CNeJU3Hqztl3HkeDIxl14Bl10zWj3dInjhkOBwteN0asAYiBXwS9+SPdg/RVF1h6ogbErhh3Vz6RiL8fu/JYjelLNnf2ceBEwNFHQDlJh8VG1bVYQARCanqbmClN82aOLE0k4aks2sCSZ680/E6MBot2KjN6opAvNSwqnqWQikiNFZXjHnyp02OvGE8lyybQ31lwGTZeMSm7daAs+tWl0byYT4if0REGoF7gPtF5OfAS140qlD403S8Jtg4SXnyAb8vXlGyEH48JEbyo9EYMS3srFBuGquDcbvmmMmRN6QgGPBx3Zo27t/ZwUjEZNkUmk072jl/UWPJzGKWT1mDP1DVblW9DfgU8B3gDR61Ky/cna3p8+TH58YD46pQwlgHZra6NbningJweLTw87u6cerXqKo9EKrak+MYpjevti2bR/YZy6aQHDk9yPajvUUfAOVmQqazqj6kqr9Q1fFzzRUBd258LE2efELHa4bBUDDWgVmwSD44FsnHZ4XyNJIP0z0YZnA0auwaQ0ouOdOxbEwtm0KyeUcHQEmkTjqURdUqd2582kie1Jk2fkkl8nYkXyCRrwmNRfJjs0J589E3VlXQPRh25ciXxi2jobQIBny8anUb9+9sZzQyJVM9zAg2b29nVVsdi2fXFLspccpD5F3C7o7q3ckxaWvX+FOIfJUTyRfm46lyRfLOrFCe2TU1VhVNJ33S2DWGdLz67DZ6hyM8ut9YNoXgRN8IT710qqSsGigXkXc9TxfJk86ukVSevCXyBYvkg34iMWU0EosPJw95ZtdUMBKJcfBkPzA10/4ZpieXnDmHulDAzBhVIH6zqwNVjMh7gTt61zQTCGUrNRxMYdcUzpN3ZoeKeB/J223fcayXqgp//AfLYEgmFPDzqtWt3LfDWDaFYNP2dhbPrmZlgaa+LBTlIfKkzq5xk1iUzP08Q8drobJrQmOVKIc9FnnHatpxtIf5TVVFmR3eMH24cd1cy7I5YCybydAzFOaxAye5fk1byX3nykLk3aSN5NMOhkrMkwdXCmWhRrwGx2rKDzkplB4MhoKxu5BDXYOmnIEhK5etsCybe7cZy2YyPLC7k3BUS2aUq5uyEPnEFMo0nbBpatf4UuTJe5FCCYmRvBdlDQCaasbsGTMQypCNUMDPtatbuW9nR8KMYob82Lyjndb6EOcuaCx2U8ZRFiLvJsGfd9k4ibVrxkfyqTz5gpU1SOHJV3qWQjlWEMlUnzTkwo3r5tp2Q1exmzItGRqN8uCeE1y3uq0gBQ0LTVmIfDphT7RuUts1vhR58k0FjuQdT35wZAo8+WoTyRvy47Llc6g1ls2EeXjfCYbC0ZLLqnHwVORFpFJEnhSRrSKyQ0Q+48VxEjteU2+TajYoSF3WwImGCx3JD4xGxuwaj0S+ssIf/wEx6ZOGXKis8HPNWS1s3tluLJsJsHl7O43VFbzijFnFbkpKvI7kR4CrVfUc4FzgehHZUOiDJKZQpo7k003/50uRXVNXGcAnhc+uGRyNMhSOEvAJFX7vPnrnTsR0vBpy5cZ1c+keDPO4sWzyIhyN8ZtdHVx7Vqun3+nJ4Gmr1KLffllhP9LE2pM4jut5LJ0n79rGl8WT9/mExupg4bJrKuxI3s6u8cqqcWioDuL3Ca11IU+PYygfrljRTE3Qb8oP58mWg130DkdKqlZNMp7PJiEifuAZ4EzgG6r6RKGP4Y7eTw2MupaPbZM45V/mAmVg5ZsXypN30iWdSL7So/RJh6bqCtrqKwmUaGRhKD0sy6aV/9t6jGM9w8VuTk601oX41GtXU19ZvAF/m7a3Ux30c9nyOUVrQzY8F3lVjQLn2rXofyYia1V1u7NeRG7FnhB80aJFEzpGOu/cHeFLmhTKS5fP4aZXLIznxju8c8PihHTEyRAM+Aj6fQyORhkJR6ms8FZ833zBgoQfO4MhF265ZAnHuofoHQoXuyk58dj+k7T3DnPHzRcWxSqJxZT7dnZw1coWz/rYCsGUzQunqt0i8gBwPbDdtfx24HaA9evXT8jKqQ4GuOv9G7jp21vSbpMg8q7/h1Vt9XzhjWeP2/6PLj1jIk1JizVxiJVC6bVd88bzF3i6f0N5ct6iJu7+k1cWuxk586OnD/Oxu7fxNz99gX9689lTPtL0ucOnOdE3wnVrSmMGqHR4nV3TbEfwiEgV8CpgtzfHyrKe8RbNVGJNHBKdEpE3GGYCb12/kA9fs5wfP3OEf/3t/ik//qbt7QT9Pq5e1TLlx84HryP5ucB/2b68D/iRqv7SiwOllO20E4hMvchbE4dYKZReVaA0GGYaH7l2OUdOD/Evv9nL/KYq3nzB1NzFqiqbdrRzyZmzqStin0AueCryqroNOM/LYzikulVzZ9ekGgA1lVQH/QyMRhkKx+JFxAwGw+QQEb7wxnW09w7xiZ9sY25DJZec6X0n6M7jvRw+NcSfX3Wm58eaLGWTfpFKtzVNJF8Mu6Y6GGBoNMLwqLFrDIZCEgz4+I93XsCy5lo+8L1n2N3e6/kxN+/owCdw7Vml7cdDGYl8Kty9uIkFyqa8KdSE/GOevMcplAbDTKO+soI7b7mQ6pCfW+58inaP00A3b2/nwiWzmF1b+mNRykbks+t2se2aQNyT9zqF0mCYicxrrOKOmy+kdyjMLd99in57XuVCc/BEP3s6+kq2Vk0yZaM2qe2a7FUop4qakOPJR0s6p9ZgmM6smdfAv7/zAvZ29PGnP3jWk1o8m3d0AJT0KFc3ZSPyqWL5RLumuJF8VUWAwRErkjeevMHgHVesaOZzb1jLw3tP8Kl7ticEe4Vg8452zl7QMG1qQ5WNyGfteHUtL1BJmrxwIvlwVE0kbzB4zNtesYgPXn0mP3zqMN94oHA59Md7hnj+cPe0ieJhCke8FoN0kby/SJ68g4nkDQbv+ctXreDI6SG+dJ+VQ/8H500+h/4+26qZLn48lJHIpx4MldqTL4Zd45QbBjwvUGYwGKwc+i++6Wzae4b52N3baK2v5JXLJpdDv2l7O8tbalnWXFugVnpPGdk1uQt3MaboMpG8wTD1BAM+vvmuC1gyu4Y//t4z7O3om/C+Tg2M8uShU9PKqoFyEvkUyxLsGpewF2cwlCuSNymUBsOU0VBl5dBXVlg59J29E8uh/82uDqIxnVZWDZSTyOfT8VqEwVBukTeRvMEwtSxoqubOmy/k9OAot3z3KQYmkEO/eXs78xurWDOv3oMWekf5iHzKFMrSqV1TEzJ2jcFQTNbOb+Abbz+f3e19/Nn/PEskjxz6/pEIv993kuvXtk15SePJUjYin4rSql0zJuymCqXBUByuWtXCZ1+/lgf3nOBTP9+Rcw79g3s6GY3Gpp0fD+WUXZO1nvwYRYnkTcerwVASvP2iRRw5Pci/P3iAhbOq+NMrs1eS3LS9nTm1QS5Y3DQFLSwsZSPyqUiM5N12zdS3JcGTNymUBkNR+evrVnLk9BD/tGkP8xureP2589NuOxyO8sDuTl537vyiuACTpWxEPmskb6/3SX7ploWi2njyBkPJ4PMJ//yWs+noHeajP7Zy6DcsnZ1y28cOnGRgNMrGEp/mLx1l48mn7nh1r7cohlUDicJuUigNhuITCvi5/V3rWTS7mlv/+2n2d6bOod+0vZ26UGDSA6mKRVmrjbtTxRH3YgyEAquz1xF6U7vGYCgNGqoruPPmCwkG/Lznjqfo7EvMoY9EY9y/s4NrzmohGJiecjk9W52CXO2aYtStcagJ+RGB0DT9ZzEYypGFs6q54+b1nBoY5b3ffTohh/7JQ6c4PRiedgOg3JSN2mQXeTuSL2K/SVXQT2XAP+3ybA2GcufsBY18/e3nseNYDx+667l4Dv19OzoIBXxcvqK5yC2cOJ6KvIgsFJEHRGSniOwQkQ97dqxUnnyKPPli2TVgpVGazBqDoTS55qxWPvO6Nfx2dye3/d8OYjFl0/Z2rljRnFB7arrhdcsjwF+p6rMiUgc8IyL3q+rOQh8oZVkDV9ers7qYKVDVQb/JrDEYSph3XbyEI91DfOuhg/QPR2jvHeZja1cWu1mTwlORV9XjwHH7eZ+I7ALmAwUX+dTHH3se73gtqicfoLIiXLTjGwyG7Hx84yqOnh7inuePEfAJ16yanqmTDlN2DyIiS4DzgCeSlt8K3AqwaNGiie/f9fy8RY0893I3t16+1HUc628xRf7sBQ3MmQazuxsMMxmfT/jSW85hYCTCrJoQDdUVxW7SpJgSkReRWuAnwEdUtde9TlVvB24HWL9+/YQnY3S0u6rCz8/+9JJx6x1x9xexq/mjG1cV7+AGgyFnKiv83HnLKwo+P2wx8FzyRKQCS+B/oKo/9fBI9vEyb1XMFEqDwTC9KIdMOK+zawT4DrBLVb/i7bFyW18OF81gMBhyxetI/hLgXcDVIvK8/bjRiwNlK1swZtcYkTcYDDMHr7NrHiHNHNteke5gpZBCaTAYDFNNGY14dfyY1OudSN64NQaDYSZRPiKfbX0J1K4xGAyGqaZ8RD5zIB+P9I1dYzAYZhLlI/JkLyUsRZowxGAwGIpF2Yi8QyYJF4o7GMpgMBimmrKRvFwCdJ+I8eQNBsOMomxE3iGTHWPsGoPBMNMoG5HP1vFqrRPT8WowGGYUZSTykvA39TYmhdJgMMwsykbkHTJpuGXXTF1bDAaDodiUjcjnot3GrjEYDDON8hH5HDx5n5jBUAaDYWZRNiLvkNmuEZNdYzAYZhTlJ/IZYnmr43UKG2MwGAxFpmxE3pmlK2Mkj7FrDAbDzKJsRD5mq3zGPHlj1xgMhhlG2Yj8WCSfXsR9Jk/eYDDMMMpG5HNBxKRQGgyGmUXZiXymQN1nBkMZDIYZhqciLyJ3iEiniGz38jiQW8crZjCUwWCYYXgdyX8XuN7jYwDujldTu8ZgMBgcPBV5VX0YOOXlMeLHsv9mt2uMyBsMhplD2XjyDVUVAFyxojntNk3VQWbVVExVkwwGg6HoBIrdABG5FbgVYNGiRRPez6yaII98/Cpa6yvTbvO9915EddA/4WMYDAbDdKPokbyq3q6q61V1fXNz+ig8FxY0VVORYRLX5roQNaGi/64ZDAbDlFF0kTcYDAaDd3idQnkX8DiwUkSOiMh7vTyewWAwGBLx1LtQ1Zu83L/BYDAYMmPsGoPBYChjjMgbDAZDGWNE3mAwGMoYI/IGg8FQxog6lb1KABE5AbyU59vmACc9aE4pMxPPGWbmec/Ec4aZed6TOefFqppyoFFJifxEEJGnVXV9sdsxlczEc4aZed4z8ZxhZp63V+ds7BqDwWAoY4zIGwwGQxlTDiJ/e7EbUARm4jnDzDzvmXjOMDPP25NznvaevMFgMBjSUw6RvMFgMBjSYETeYDAYyphpK/Iicr2I7BGR/SLyiWK3p1CIyEIReUBEdorIDhH5sL18lojcLyL77L9N9nIRkX+1P4dtInJ+cc9gcoiIX0SeE5Ff2q/PEJEn7PP7XxEJ2stD9uv99volRW34BBGRRhG5W0R2i8guEbl4JlxrEfkL+/97u4jcJSKV5XitReQOEekUke2uZXlfXxF5j739PhF5Tz5tmJYiLyJ+4BvADcBq4CYRWV3cVhWMCPBXqroa2AD8mX1unwB+q6rLgd/ar8H6DJbbj1uB/5j6JheUDwO7XK+/CPyLqp4JnAacctXvBU7by//F3m468jVgk6quAs7BOveyvtYiMh/4ELBeVdcCfuBtlOe1/i5wfdKyvK6viMwCPg1cBLwC+LTzw5ATqjrtHsDFwGbX608Cnyx2uzw6158DrwL2AHPtZXOBPfbzbwE3ubaPbzfdHsAC+5/+auCXgGCNAAwkX3dgM3Cx/TxgbyfFPoc8z7cBeDG53eV+rYH5wGFgln3tfglsLNdrDSwBtk/0+gI3Ad9yLU/YLttjWkbyjP2TOByxl5UV9m3pecATQKuqHrdXtQOt9vNy+iy+CnwMiNmvZwPdqhqxX7vPLX7e9voee/vpxBnACeBO26L6TxGpocyvtaoeBb4EvAwcx7p2z1De19pNvtd3Utd9uop82SMitcBPgI+oaq97nVo/52WV+yoirwE6VfWZYrdlCgkA5wP/oarnAQOM3boDZXutm4DXY/3IzQNqGG9pzAim4vpOV5E/Cix0vV5gLysLRKQCS+B/oKo/tRd3iMhce/1coNNeXi6fxSXA60TkEPBDLMvma0CjiDgzmLnPLX7e9voGoGsqG1wAjgBHVPUJ+/XdWKJf7tf6WuBFVT2hqmHgp1jXv5yvtZt8r++krvt0FfmngOV2b3wQq9PmF0VuU0EQEQG+A+xS1a+4Vv0CcHrV34Pl1TvL3233zG8Aely3gtMGVf2kqi5Q1SVY1/N3qvoO4AHgzfZmyeftfB5vtrefVhGvqrYDh0Vkpb3oGmAnZX6tsWyaDSJSbf+/O+ddttc6iXyv72bgOhFpsu+CrrOX5UaxOyUm0ZlxI7AXOAD8bbHbU8DzuhTr9m0b8Lz9uBHLg/wtsA/4DTDL3l6wMo0OAC9gZSwU/Twm+RlcCfzSfr4UeBLYD/wYCNnLK+3X++31S4vd7gme67nA0/b1vgdomgnXGvgMsBvYDnwPCJXjtQbuwup3CGPdub13ItcX+CP7/PcDt+TTBlPWwGAwGMqY6WrXGAwGgyEHjMgbDAZDGWNE3mAwGMoYI/IGg8FQxhiRN5QFInKziMwrdjsmgohcKSKvLHY7DOWJEXlDuXAz1uhJT3AN0vGCK4G8RN7j9hjKCJNCaShZ7DouP8Ia4ecHPouVJ/wVoBarUNXNWKMlv4s1CnAIq5jVUIr9HbL3d4O93dtVdb+IvBb4f0AQayTlO1S1Q0RuA5Zh5W+/jFUI73tYw/AB/lxVHxORK7HyvruBdfYxXsCqqFkFvEFVD4hIM/BNYJH9/o/Ybd4CRLHq2HwQK388YTtVfTS5Pap6Ux4fp2GmUuzBAuZhHukewJuAb7teNwCPAc326z8E7rCfP0iWwUHAIeyBc8C7GRtw1cRYwPM+4Mv289uwCmdV2a+rgUr7+XLgafv5lVgCPxdrUM9R4DP2ug8DX7Wf/w9wqf18EdaoZuc4f+1qZ6bt4u0xD/PI5WFu+QylzAvAl0Xki1jlaE8Da4H7rdHw+LFGE+bDXa6//2I/XwD8r11HJIhV/tfhFzp2V1ABfF1EzsWKvFe4tntK7RIDInIAuM91DlfZz68FVtttB6i3C9Elk2k7d3sMhqwYkTeULKq6154d50bgH4DfATtU9eLJ7DbF838DvqKqv7Ctl9tc2wy4nv8F0IE1uYcPGHatG3E9j7lexxj7nvmADarqfh8uMSeH7QaSNzYYMmE6Xg0li50tM6iq3wf+GWtmnGYRudheXyEia+zN+4C6HHb7h66/j9vPGxir6pdparUG4LiqxoB3Yd1J5MN9WJ47APYdAYxve7rtDIa8MSJvKGXWAU+KyPNY05/9HVYVwi+KyFas4m1OVsp3gW+KyPMiUpVhn00isg3LK/8Le9ltwI9F5Bmsztx0/DvwHvvYq8g/qv4QsN6ev3Mn8AF7+f8Bf2C3/bIM2xkMeWOyawwzBju7Zr2qZhJyg6GsMJG8wWAwlDGm49VQdojIz7CmlnPzcbUmJDEYZhTGrjEYDIYyxtg1BoPBUMYYkTcYDIYyxoi8wWAwlDFG5A0Gg6GMMSJvMBgMZYwReYPBYChj/j+v8vwFW1re/wAAAABJRU5ErkJggg==", + "image/png": "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", "text/plain": [ "
" ] @@ -359,14 +362,14 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Starting experimental run with id: 20. \n" + "Starting experimental run with id: 7. \n" ] } ], @@ -381,12 +384,12 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -413,9 +416,21 @@ }, { "cell_type": "code", - "execution_count": 98, + "execution_count": 11, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'scipy'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32mc:\\Users\\Serwan\\Documents\\Github\\Qcodes_Sydney\\docs\\examples\\DataSet\\MeasurementLoop.ipynb Cell 30\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mscipy\u001b[39;00m \u001b[39mimport\u001b[39;00m optimize\n\u001b[0;32m 4\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mretune_device\u001b[39m():\n\u001b[0;32m 5\u001b[0m \u001b[39mwith\u001b[39;00m MeasurementLoop(\u001b[39m'\u001b[39m\u001b[39mretune_device\u001b[39m\u001b[39m'\u001b[39m) \u001b[39mas\u001b[39;00m msmt:\n\u001b[0;32m 6\u001b[0m \u001b[39m# Create a random minimal point\u001b[39;00m\n", + "\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'scipy'" + ] + } + ], "source": [ "from scipy import optimize\n", "\n", @@ -463,7 +478,7 @@ }, { "cell_type": "code", - "execution_count": 99, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -481,7 +496,7 @@ }, { "cell_type": "code", - "execution_count": 100, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -528,7 +543,7 @@ }, { "cell_type": "code", - "execution_count": 101, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -585,7 +600,7 @@ }, { "cell_type": "code", - "execution_count": 102, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -632,7 +647,7 @@ }, { "cell_type": "code", - "execution_count": 105, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -673,7 +688,7 @@ }, { "cell_type": "code", - "execution_count": 106, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -726,7 +741,7 @@ }, { "cell_type": "code", - "execution_count": 110, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -750,7 +765,7 @@ }, { "cell_type": "code", - "execution_count": 109, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -791,7 +806,7 @@ }, { "cell_type": "code", - "execution_count": 113, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -856,7 +871,7 @@ }, { "cell_type": "code", - "execution_count": 123, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -884,7 +899,7 @@ }, { "cell_type": "code", - "execution_count": 125, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -921,7 +936,7 @@ }, { "cell_type": "code", - "execution_count": 129, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -956,7 +971,7 @@ }, { "cell_type": "code", - "execution_count": 134, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -984,7 +999,7 @@ }, { "cell_type": "code", - "execution_count": 136, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1012,7 +1027,7 @@ }, { "cell_type": "code", - "execution_count": 137, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1041,7 +1056,7 @@ }, { "cell_type": "code", - "execution_count": 140, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1077,7 +1092,7 @@ }, { "cell_type": "code", - "execution_count": 144, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1104,7 +1119,7 @@ }, { "cell_type": "code", - "execution_count": 145, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1152,7 +1167,7 @@ }, { "cell_type": "code", - "execution_count": 146, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -1160,7 +1175,7 @@ "output_type": "stream", "text": [ "Initial value get_parameter()=2\n", - "Starting experimental run with id: 42. \n", + "Starting experimental run with id: 8. \n", "Masked value get_parameter()=9\n", "Value after measurement finished: get_parameter()=2\n" ] @@ -1210,7 +1225,7 @@ }, { "cell_type": "code", - "execution_count": 149, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -1221,7 +1236,7 @@ }, { "cell_type": "code", - "execution_count": 154, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -1230,7 +1245,7 @@ "text": [ "Initial object value my_object.object_attribute=42\n", "Initial dictionary d={'key1': 12, 'key2': 13, 'key3': 14}\n", - "Starting experimental run with id: 44. \n", + "Starting experimental run with id: 9. \n", "Masked object value my_object.object_attribute=999\n", "Masked dictionary d={'key1': 12, 'key2': 999, 'key3': 14}\n", "Final object value my_object.object_attribute=42\n", @@ -1273,7 +1288,7 @@ }, { "cell_type": "code", - "execution_count": 157, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1315,7 +1330,7 @@ }, { "cell_type": "code", - "execution_count": 160, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1360,7 +1375,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3.9.5 ('base')", + "display_name": "Python 3.10.4 ('qcodes-sydney')", "language": "python", "name": "python3" }, @@ -1374,12 +1389,12 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.5" + "version": "3.10.4" }, "orig_nbformat": 4, "vscode": { "interpreter": { - "hash": "19d1d53a962d236aa061289c2ac16dc8e6d9648c89fe79f459ae9a3493bc67b4" + "hash": "5489eaf2c0162c90544bb6633d254e9aaec572698f0919090a3f0c8d3f72ceff" } } }, diff --git a/qcodes/dataset/__init__.py b/qcodes/dataset/__init__.py index b4e4953f811..fc761da8aeb 100644 --- a/qcodes/dataset/__init__.py +++ b/qcodes/dataset/__init__.py @@ -25,6 +25,7 @@ ) from .experiment_settings import get_default_experiment_id, reset_default_experiment_id from .legacy_import import import_dat_file +from .measurement_loop import MeasurementLoop, Sweep from .measurements import Measurement from .plotting import plot_by_id, plot_dataset from .sqlite.database import ( @@ -73,9 +74,11 @@ "load_from_netcdf", "load_last_experiment", "load_or_create_experiment", + "MeasurementLoop", "new_data_set", "new_experiment", "plot_by_id", "plot_dataset", "reset_default_experiment_id", + "Sweep" ] From b6616057488d33e7f8774091a9fb44a4612a228a Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 10 Aug 2022 10:32:22 +0200 Subject: [PATCH 040/122] BaseSweep is subclass of AbstractSweep --- qcodes/dataset/measurement_loop.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 1fc0f24cef1..6fe7b22c57d 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -12,13 +12,15 @@ from qcodes.dataset.descriptions.rundescriber import RunDescriber from qcodes.dataset.descriptions.versioning import serialization as serial from qcodes.dataset.descriptions.versioning.converters import new_to_old +from qcodes.dataset.do_nd import AbstractSweep from qcodes.dataset.measurements import Measurement from qcodes.dataset.sqlite.queries import add_parameter, update_run_description from qcodes.instrument.base import InstrumentBase from qcodes.instrument.parameter import _BaseParameter, DelegateParameter, MultiParameter, Parameter from qcodes.instrument.sweep_values import SweepValues +from qcodes.parameters.parameter_base import ParameterBase from qcodes.station import Station -from qcodes.utils.dataset.doNd import AbstractSweep +from qcodes.utils.dataset.doNd import AbstractSweep, ActionsT from qcodes.utils.helpers import ( PerformanceTimer, directly_executed_from_cell, @@ -1207,7 +1209,7 @@ def __next__(self): -class BaseSweep: +class BaseSweep(AbstractSweep): """Sweep over an iterable inside a Measurement Args: @@ -1437,6 +1439,27 @@ def execute( with MeasurementLoop(name) as msmt: measure_sweeps(sweeps=sweeps, measure_params=measure_params, msmt=msmt) + # Methods needed to make BaseSweep subclass of AbstractSweep + def get_setpoints(self) -> np.ndarray: + return self.sequence + + @property + def param(self) -> ParameterBase: + # TODO create necessary parameter if self.parameter is None + return self.parameter + + @property + def num_points(self) -> float: + return len(self.sequence) + + @property + def post_actions(self) -> ActionsT: + # TODO maybe add option for post actions + # However this can cause issues if sweep is prematurely exited + return None + + + class Sweep(BaseSweep): sequence_keywords = ['start', 'stop', 'around', 'num', 'step', 'parameter', 'sequence'] From 71795d496a84250208b7412cada0dc5b210c29c8 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 10 Aug 2022 11:13:28 +0200 Subject: [PATCH 041/122] fixed bugs of BaseSweep --- qcodes/dataset/measurement_loop.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 6fe7b22c57d..9fe5dc126fe 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1249,7 +1249,7 @@ def __init__(self, sequence, name=None, label=None, unit=None, parameter=None, r self.loop_index = None self.iterator = None self.revert = revert - self.delay = delay + self._delay = delay self.initial_delay = initial_delay # setpoint_info will be populated once the sweep starts @@ -1452,11 +1452,18 @@ def param(self) -> ParameterBase: def num_points(self) -> float: return len(self.sequence) + @property + def delay(self) -> float: + """ + Delay between two consecutive sweep points. + """ + return self._delay or 0 + @property def post_actions(self) -> ActionsT: # TODO maybe add option for post actions # However this can cause issues if sweep is prematurely exited - return None + return [] From 8f165d9d0d86b3f5add02baf0feee454063f53b3 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 10 Aug 2022 11:17:46 +0200 Subject: [PATCH 042/122] Add args to sweep.execute --- qcodes/dataset/measurement_loop.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 9fe5dc126fe..cb6f6625c93 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1398,6 +1398,7 @@ def exit_sweep(self): def execute( self, + *args: Iterable['BaseSweep'], name: str = None, measure_params: Iterable = None, repetitions: int = 1, @@ -1413,14 +1414,14 @@ def execute( ) measure_params = station.measure_params - # Create list of sweeps + sweeps = list(args) + if not all(isinstance(sweep, BaseSweep) for sweep in sweeps): + raise ValueError('Args passed to Sweep.execute must be Sweeps') if isinstance(sweep, BaseSweep): - sweeps = [sweep] + sweeps.append(sweep) elif isinstance(sweep, (list, tuple)): - sweeps = list(sweep) - elif sweep is None: - sweeps = [] + sweeps += list(sweep) # Add repetition as a sweep if > 1 if repetitions > 1: From 744901fe99c29bbffa17d2091f770d72ce34cbda Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 10 Aug 2022 20:45:54 +0200 Subject: [PATCH 043/122] fix: all tests working again --- qcodes/dataset/data_set_in_memory.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qcodes/dataset/data_set_in_memory.py b/qcodes/dataset/data_set_in_memory.py index c1f809008bc..2ccf8b1231f 100644 --- a/qcodes/dataset/data_set_in_memory.py +++ b/qcodes/dataset/data_set_in_memory.py @@ -404,13 +404,14 @@ def prepare( shapes: Shapes = None, parent_datasets: Sequence[Mapping[Any, Any]] = (), write_in_background: bool = False, + allow_empty_dataset: bool = False ) -> None: if not self.pristine: raise RuntimeError("Cannot prepare a dataset that is not pristine.") self.add_snapshot(json.dumps({"station": snapshot}, cls=NumpyJSONEncoder)) - if interdeps == InterDependencies_(): + if interdeps == InterDependencies_() and not allow_empty_dataset: raise RuntimeError("No parameters supplied") self._set_interdependencies(interdeps, shapes) From f6a2b095c31d3cf9104e7ca8f40499d91029781f Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 10 Aug 2022 21:04:51 +0200 Subject: [PATCH 044/122] add some basic tests --- qcodes/dataset/measurement_loop.py | 8 +++- .../test_measurement_loop_basics.py | 33 ++++++------- .../test_measurement_loop_sweep.py | 46 ++++++++++++++++++- 3 files changed, 69 insertions(+), 18 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index cb6f6625c93..e30e903e558 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1400,7 +1400,7 @@ def execute( self, *args: Iterable['BaseSweep'], name: str = None, - measure_params: Iterable = None, + measure_params: Union[Iterable, _BaseParameter] = None, repetitions: int = 1, sweep: Union[Iterable, 'BaseSweep'] = None ): @@ -1413,6 +1413,10 @@ def execute( 'Either provide measure_params, or set station.measure_params' ) measure_params = station.measure_params + + # Convert measure_params to list if it is a single param + if isinstance(measure_params, _BaseParameter): + measure_params = [measure_params] # Create list of sweeps sweeps = list(args) @@ -1440,6 +1444,8 @@ def execute( with MeasurementLoop(name) as msmt: measure_sweeps(sweeps=sweeps, measure_params=measure_params, msmt=msmt) + return msmt.dataset + # Methods needed to make BaseSweep subclass of AbstractSweep def get_setpoints(self) -> np.ndarray: return self.sequence diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index f7f7e907949..d2697e7ef7c 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -13,27 +13,28 @@ from qcodes.utils.dataset.doNd import LinSweep -@pytest.fixture -def create_dummy_database(): - @contextlib.contextmanager - def func_context_manager(): - with tempfile.TemporaryDirectory() as temporary_folder: - temporary_folder = tempfile.TemporaryDirectory() - print(f"Created temporary folder for database: {temporary_folder}") +# @pytest.fixture +# def create_dummy_database(): +# @contextlib.contextmanager +# def func_context_manager(): +# with tempfile.TemporaryDirectory() as temporary_folder: +# temporary_folder = tempfile.TemporaryDirectory() +# print(f"Created temporary folder for database: {temporary_folder}") - assert Path(temporary_folder.name).exists() - db_path = Path(temporary_folder.name) / "test_database.db" - initialise_or_create_database_at(str(db_path)) +# assert Path(temporary_folder.name).exists() +# db_path = Path(temporary_folder.name) / "test_database.db" +# initialise_or_create_database_at(str(db_path)) - try: - exp = load_or_create_experiment("test_experiment") - yield exp - finally: - exp.conn.close() +# try: +# exp = load_or_create_experiment("test_experiment") +# yield exp +# finally: +# exp.conn.close() - return func_context_manager +# return func_context_manager +@pytest.mark.usefixtures("empty_temp_db", "experiment") def test_original_dond(create_dummy_database): with create_dummy_database(): from qcodes.utils.dataset.doNd import LinSweep, dond diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index 0b61e7f9905..12ca8538074 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -10,7 +10,7 @@ from qcodes.dataset import initialise_or_create_database_at, load_or_create_experiment from qcodes.dataset.data_set import load_by_id from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep -from qcodes.utils.dataset.doNd import LinSweep +from qcodes.utils.dataset.doNd import LinSweep, dond def test_sweep_1_arg_sequence(): @@ -122,3 +122,47 @@ def test_error_on_iterate_sweep(): with pytest.raises(RuntimeError): iter(sweep) + + +@pytest.mark.usefixtures("empty_temp_db", "experiment") +def test_sweep_in_dond(): + set_parameter = ManualParameter('set_param') + sweep = Sweep(set_parameter, [1,2,3]) + get_parameter = Parameter('get_param', get_cmd=set_parameter) + + dataset, _, _ = dond(sweep, get_parameter) + assert np.allclose(dataset.get_parameter_data('get_param')['get_param']['get_param'], [1,2,3]) + + +@pytest.mark.usefixtures("empty_temp_db", "experiment") +def test_sweep_and_linsweep_in_dond(): + set_parameter = ManualParameter('set_param') + + sweep = Sweep(set_parameter, [1,2,3]) + + set_parameter2 = ManualParameter('set_param2') + linsweep = LinSweep(set_parameter2, 0, 10, 11) + get_parameter = Parameter('get_param', get_cmd=set_parameter) + + dataset, _, _ = dond(sweep, linsweep, get_parameter) + arr = dataset.get_parameter_data('get_param')['get_param']['get_param'] + + assert np.allclose(arr, np.repeat(np.array([1,2,3])[:,np.newaxis], 11, axis=1)) + + +def test_sweep_execute_sweep_args(): + set_parameter = ManualParameter('set_param') + sweep = Sweep(set_parameter, [1,2,3]) + set_parameter2 = ManualParameter('set_param2') + other_sweep = Sweep(set_parameter2, [1,2,3]) + + get_param = Parameter( + 'get_param', + get_cmd=lambda: set_parameter() + set_parameter2()) + + dataset = sweep.execute(other_sweep, measure_params=get_param) + + arr = dataset.get_parameter_data('get_param')['get_param']['get_param'] + assert np.allclose(arr, [[2,3,4], [3,4,5], [4,5,6]]) + print(dataset) + From 491669751f4dcbfd5d3e90b542abc93f8d3d239e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Aug 2022 19:05:16 +0000 Subject: [PATCH 045/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 2 +- .../measurement_loop/test_measurement_loop_sweep.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index e30e903e558..e8e28521352 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1413,7 +1413,7 @@ def execute( 'Either provide measure_params, or set station.measure_params' ) measure_params = station.measure_params - + # Convert measure_params to list if it is a single param if isinstance(measure_params, _BaseParameter): measure_params = [measure_params] diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index 12ca8538074..aeaf80fc503 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -137,7 +137,7 @@ def test_sweep_in_dond(): @pytest.mark.usefixtures("empty_temp_db", "experiment") def test_sweep_and_linsweep_in_dond(): set_parameter = ManualParameter('set_param') - + sweep = Sweep(set_parameter, [1,2,3]) set_parameter2 = ManualParameter('set_param2') @@ -146,7 +146,7 @@ def test_sweep_and_linsweep_in_dond(): dataset, _, _ = dond(sweep, linsweep, get_parameter) arr = dataset.get_parameter_data('get_param')['get_param']['get_param'] - + assert np.allclose(arr, np.repeat(np.array([1,2,3])[:,np.newaxis], 11, axis=1)) @@ -157,12 +157,11 @@ def test_sweep_execute_sweep_args(): other_sweep = Sweep(set_parameter2, [1,2,3]) get_param = Parameter( - 'get_param', - get_cmd=lambda: set_parameter() + set_parameter2()) + "get_param", get_cmd=lambda: set_parameter() + set_parameter2() + ) dataset = sweep.execute(other_sweep, measure_params=get_param) arr = dataset.get_parameter_data('get_param')['get_param']['get_param'] assert np.allclose(arr, [[2,3,4], [3,4,5], [4,5,6]]) print(dataset) - From b4b5384dcfaa7f5330a492d7f53ec4f82bdea9f1 Mon Sep 17 00:00:00 2001 From: Serwan Date: Mon, 15 Aug 2022 17:22:12 +0200 Subject: [PATCH 046/122] add len(sweep) --- qcodes/dataset/measurement_loop.py | 3 +++ .../dataset/measurement_loop/test_measurement_loop_sweep.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index e8e28521352..1f53caa24e7 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1277,6 +1277,9 @@ def __repr__(self): components_str = ', '.join(components) return f'Sweep({components_str})' + def __len__(self): + return len(self.sequence) + def __iter__(self): if threading.current_thread() is not MeasurementLoop.measurement_thread: raise RuntimeError( diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index aeaf80fc503..7037432c6a2 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -117,6 +117,10 @@ def test_sweep_step(): assert np.allclose(sweep.sequence, np.append(np.arange(0, 9.9, 0.5), [9.9])) +def test_sweep_len(): + sweep = Sweep(start=0, stop=10, step=0.5) + assert len(sweep) == 21 + def test_error_on_iterate_sweep(): sweep = Sweep([1,2,3], 'sweep') From 384d0e145fcdbaa3b355164881613df812ffdc71 Mon Sep 17 00:00:00 2001 From: Serwan Date: Sun, 21 Aug 2022 17:13:34 +0200 Subject: [PATCH 047/122] improve database for tests --- .../test_measurement_loop_basics.py | 214 ++++++++---------- 1 file changed, 95 insertions(+), 119 deletions(-) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index d2697e7ef7c..272f257ee4c 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -13,57 +13,33 @@ from qcodes.utils.dataset.doNd import LinSweep -# @pytest.fixture -# def create_dummy_database(): -# @contextlib.contextmanager -# def func_context_manager(): -# with tempfile.TemporaryDirectory() as temporary_folder: -# temporary_folder = tempfile.TemporaryDirectory() -# print(f"Created temporary folder for database: {temporary_folder}") - -# assert Path(temporary_folder.name).exists() -# db_path = Path(temporary_folder.name) / "test_database.db" -# initialise_or_create_database_at(str(db_path)) - -# try: -# exp = load_or_create_experiment("test_experiment") -# yield exp -# finally: -# exp.conn.close() - -# return func_context_manager - - @pytest.mark.usefixtures("empty_temp_db", "experiment") -def test_original_dond(create_dummy_database): - with create_dummy_database(): - from qcodes.utils.dataset.doNd import LinSweep, dond - - p1_get = ManualParameter("p1_get", initial_value=1) - p2_get = ManualParameter("p2_get", initial_value=1) - p1_set = ManualParameter("p1_set", initial_value=1) - dond( - p1_set, 0, 1, 101, - p1_get, p2_get - ) +def test_original_dond(): + from qcodes.utils.dataset.doNd import LinSweep, dond + + p1_get = ManualParameter("p1_get", initial_value=1) + p2_get = ManualParameter("p2_get", initial_value=1) + p1_set = ManualParameter("p1_set", initial_value=1) + dond( + p1_set, 0, 1, 101, + p1_get, p2_get + ) -def test_create_measurement(create_dummy_database): - with create_dummy_database(): - MeasurementLoop("test") +def test_create_measurement(): + MeasurementLoop("test") -def test_basic_1D_measurement(create_dummy_database): - with create_dummy_database(): - # Initialize parameters - p1_get = ManualParameter("p1_get") - p1_set = ManualParameter("p1_set") +def test_basic_1D_measurement(): + # Initialize parameters + p1_get = ManualParameter("p1_get") + p1_set = ManualParameter("p1_set") - with MeasurementLoop("test") as msmt: - for val in Sweep(p1_set, 0, 1, 11): - assert p1_set() == val - p1_get(val + 1) - msmt.measure(p1_get) + with MeasurementLoop("test") as msmt: + for val in Sweep(p1_set, 0, 1, 11): + assert p1_set() == val + p1_get(val + 1) + msmt.measure(p1_get) data = msmt.dataset assert data.name == "test" @@ -76,20 +52,19 @@ def test_basic_1D_measurement(create_dummy_database): assert np.allclose(data_arrays["p1_set"], np.linspace(0, 1, 11)) -def test_basic_2D_measurement(create_dummy_database): - with create_dummy_database(): - # Initialize parameters - p1_get = ManualParameter("p1_get") - p1_set = ManualParameter("p1_set") - p2_set = ManualParameter("p2_set") +def test_basic_2D_measurement(): + # Initialize parameters + p1_get = ManualParameter("p1_get") + p1_set = ManualParameter("p1_set") + p2_set = ManualParameter("p2_set") - with MeasurementLoop("test") as msmt: - for val in Sweep(p1_set, 0, 1, 11): - assert p1_set() == val - for val2 in Sweep(p2_set, 0, 1, 11): - assert p2_set() == val2 - p1_get(val + 1) - msmt.measure(p1_get) + with MeasurementLoop("test") as msmt: + for val in Sweep(p1_set, 0, 1, 11): + assert p1_set() == val + for val2 in Sweep(p2_set, 0, 1, 11): + assert p2_set() == val2 + p1_get(val + 1) + msmt.measure(p1_get) data = msmt.dataset assert data.name == "test" @@ -109,19 +84,18 @@ def test_basic_2D_measurement(create_dummy_database): ) -def test_1D_measurement_duplicate_get(create_dummy_database): - with create_dummy_database(): - # Initialize parameters - p1_get = ManualParameter("p1_get") - p1_set = ManualParameter("p1_set") +def test_1D_measurement_duplicate_get(): + # Initialize parameters + p1_get = ManualParameter("p1_get") + p1_set = ManualParameter("p1_set") - with MeasurementLoop("test") as msmt: - for val in Sweep(p1_set, 0, 1, 11): - assert p1_set() == val - p1_get(val + 1) - msmt.measure(p1_get) - p1_get(val + 0.5) - msmt.measure(p1_get) + with MeasurementLoop("test") as msmt: + for val in Sweep(p1_set, 0, 1, 11): + assert p1_set() == val + p1_get(val + 1) + msmt.measure(p1_get) + p1_get(val + 0.5) + msmt.measure(p1_get) data = msmt.dataset assert data.name == "test" @@ -137,21 +111,20 @@ def test_1D_measurement_duplicate_get(create_dummy_database): assert np.allclose(data_arrays["p1_set"], np.linspace(0, 1, 11)) -def test_1D_measurement_duplicate_getset(create_dummy_database): - with create_dummy_database(): - # Initialize parameters - p1_get = ManualParameter("p1_get") - p1_set = ManualParameter("p1_set") +def test_1D_measurement_duplicate_getset(): + # Initialize parameters + p1_get = ManualParameter("p1_get") + p1_set = ManualParameter("p1_set") - with MeasurementLoop("test") as msmt: - for val in Sweep(p1_set, 0, 1, 11): - assert p1_set() == val - p1_get(val + 1) - msmt.measure(p1_get) - for val in Sweep(p1_set, 0, 1, 11): - assert p1_set() == val - p1_get(val + 0.5) - msmt.measure(p1_get) + with MeasurementLoop("test") as msmt: + for val in Sweep(p1_set, 0, 1, 11): + assert p1_set() == val + p1_get(val + 1) + msmt.measure(p1_get) + for val in Sweep(p1_set, 0, 1, 11): + assert p1_set() == val + p1_get(val + 0.5) + msmt.measure(p1_get) data = msmt.dataset assert data.name == "test" @@ -171,36 +144,34 @@ def test_1D_measurement_duplicate_getset(create_dummy_database): assert np.allclose(data_arrays[set_key], np.linspace(0, 1, 11)) -def test_2D_measurement_initialization(create_dummy_database): - with create_dummy_database(): - # Initialize parameters - p1_get = ManualParameter("p1_get") - p1_set = ManualParameter("p1_set") - p2_set = ManualParameter("p2_set") +def test_2D_measurement_initialization(): + # Initialize parameters + p1_get = ManualParameter("p1_get") + p1_set = ManualParameter("p1_set") + p2_set = ManualParameter("p2_set") - with MeasurementLoop("test") as msmt: - outer_sweep = Sweep(p1_set, 0, 1, 11) - for k, val in enumerate(outer_sweep): - assert p1_set() == val + with MeasurementLoop("test") as msmt: + outer_sweep = Sweep(p1_set, 0, 1, 11) + for k, val in enumerate(outer_sweep): + assert p1_set() == val - for val2 in Sweep(p2_set, 0, 1, 11): - assert p2_set() == val2 - p1_get(val + 1) - msmt.measure(p1_get) + for val2 in Sweep(p2_set, 0, 1, 11): + assert p2_set() == val2 + p1_get(val + 1) + msmt.measure(p1_get) -def test_initialize_empty_dataset(create_dummy_database): +def test_initialize_empty_dataset(): from qcodes import Measurement - with create_dummy_database(): - msmt = Measurement() - # msmt.register_parameter(p1_set) - # msmt.register_parameter(p1_get, setpoints=(p1_set,)) - with msmt.run(allow_empty_dataset=True) as datasaver: - pass + msmt = Measurement() + # msmt.register_parameter(p1_set) + # msmt.register_parameter(p1_get, setpoints=(p1_set,)) + with msmt.run(allow_empty_dataset=True) as datasaver: + pass -def test_nested_measurement(create_dummy_database): +def test_nested_measurement(): def nested_measurement(): # Initialize parameters p1_get = ManualParameter("p1_get") @@ -212,15 +183,13 @@ def nested_measurement(): p1_get(val + 1) msmt.measure(p1_get) + # Initialize parameters + p2_set = ManualParameter("p2_set") - with create_dummy_database(): - # Initialize parameters - p2_set = ManualParameter("p2_set") - - with MeasurementLoop("test") as msmt: - for val2 in Sweep(p2_set, 0, 1, 11): - assert p2_set() == val2 - nested_measurement() + with MeasurementLoop("test") as msmt: + for val2 in Sweep(p2_set, 0, 1, 11): + assert p2_set() == val2 + nested_measurement() data = msmt.dataset assert data.name == "test" @@ -240,11 +209,10 @@ def nested_measurement(): ) -def test_measurement_no_parameter(create_dummy_database): - with create_dummy_database(): - with MeasurementLoop("test") as msmt: - for val in Sweep(np.linspace(0, 1, 11), 'p1_set', label='p1 label', unit='V'): - msmt.measure(val+1, name='p1_get') +def test_measurement_no_parameter(): + with MeasurementLoop("test") as msmt: + for val in Sweep(np.linspace(0, 1, 11), 'p1_set', label='p1 label', unit='V'): + msmt.measure(val+1, name='p1_get') data = msmt.dataset assert data.name == "test" @@ -255,3 +223,11 @@ def test_measurement_no_parameter(create_dummy_database): assert np.allclose(data_arrays["p1_get"], np.linspace(1, 2, 11)) assert np.allclose(data_arrays["p1_set"], np.linspace(0, 1, 11)) + + +# def test_measurement_percentage_complete(): +# with MeasurementLoop("test") as msmt: +# for val in Sweep(np.linspace(0, 1, 11), 'p1_set'): +# print(msmt.percentage_complete()) +# msmt.measure(val+1, name='p1_get') +# print(msmt.percentage_complete()) \ No newline at end of file From da536efb22997edee327ae512e93941b96579494 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 21 Aug 2022 15:13:58 +0000 Subject: [PATCH 048/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../dataset/measurement_loop/test_measurement_loop_basics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 272f257ee4c..5742f4a4786 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -230,4 +230,4 @@ def test_measurement_no_parameter(): # for val in Sweep(np.linspace(0, 1, 11), 'p1_set'): # print(msmt.percentage_complete()) # msmt.measure(val+1, name='p1_get') -# print(msmt.percentage_complete()) \ No newline at end of file +# print(msmt.percentage_complete()) From 9d38998596ca8089ffedeb445d18696d5b15ba8e Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 13 Sep 2022 14:54:11 +0200 Subject: [PATCH 049/122] Apply pep8 formatting to code --- qcodes/dataset/measurement_loop.py | 111 ++++++++++++++++++----------- 1 file changed, 71 insertions(+), 40 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 1f53caa24e7..230063fd133 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -28,11 +28,13 @@ using_ipython, ) -RAW_VALUE_TYPES = (float, int, bool, np.ndarray, np.integer, np.floating, np.bool_, type(None)) +RAW_VALUE_TYPES = (float, int, bool, np.ndarray, np.integer, + np.floating, np.bool_, type(None)) class DatasetHandler: """Handler for a single DataSet (with Measurement and Runner)""" + def __init__(self, measurement_loop, name='results'): self.measurement_loop = measurement_loop self.name = name @@ -105,7 +107,7 @@ def _ensure_unique_parameter(self, parameter_info, setpoint, max_idx=99): else: raise OverflowError( f'All parameter names {parameter_name}_{{idx}} up to idx {max_idx} are taken' - ) + ) # Create a delegate parameter with modified name delegate_parameter = DelegateParameter( name=parameter_name, @@ -125,7 +127,8 @@ def create_measurement_info( 'label': label, 'unit': unit } - overwrite_attrs = {key: val for key, val in overwrite_attrs.items() if val is not None} + overwrite_attrs = {key: val for key, + val in overwrite_attrs.items() if val is not None} parameter = DelegateParameter( source=parameter, **overwrite_attrs @@ -174,7 +177,7 @@ def add_measurement_result( name: str = None, label: str = None, unit: str = None, - ): + ): """Store single measurement result This method is called from type-specific methods, such as @@ -235,7 +238,8 @@ def _update_interdependencies(self): continue self._ensure_unique_parameter(setpoint_info, setpoint=True) - self.measurement.register_parameter(setpoint_info['dataset_parameter']) + self.measurement.register_parameter( + setpoint_info['dataset_parameter']) setpoint_info['registered'] = True # Register all measurement parameters in Measurement @@ -259,7 +263,8 @@ def _update_interdependencies(self): measurement_info['registered'] = True self.measurement.set_shapes( detect_shape_of_measurement( - (measurement_info["dataset_parameter"],), measurement_info["shape"] + (measurement_info["dataset_parameter"], + ), measurement_info["shape"] ) ) @@ -293,6 +298,7 @@ def _update_interdependencies(self): for key, val in interdeps_empty_dict.items(): cache_data.setdefault(key, val) + class MeasurementLoop: """Class to perform measurements @@ -508,7 +514,6 @@ def __enter__(self): shell.user_ns[self._default_measurement_name] = self # shell.user_ns[self._default_dataset_name] = self.dataset - return self except: # An error has occured, ensure running_measurement is cleared @@ -531,9 +536,11 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb): MeasurementLoop.running_measurement = None if exc_type is not None: - self.log(f"Measurement error {exc_type.__name__}({exc_val})", level="error") + self.log( + f"Measurement error {exc_type.__name__}({exc_val})", level="error") - self._apply_actions(self.except_actions, label="except", clear=True) + self._apply_actions(self.except_actions, + label="except", clear=True) if msmt is self: self._apply_actions( @@ -547,7 +554,8 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb): if msmt is self: # Also perform global final actions # These are always performed when outermost measurement finishes - self._apply_actions(MeasurementLoop.final_actions, label="global final") + self._apply_actions( + MeasurementLoop.final_actions, label="global final") # Notify that measurement is complete if self.notify and self.notify_function is not None: @@ -713,7 +721,8 @@ def _measure_callable(self, callable, name=None, **kwargs): elif hasattr(callable, "__name__"): name = callable.__name__ else: - action_indices_str = "_".join(str(idx) for idx in self.action_indices) + action_indices_str = "_".join(str(idx) + for idx in self.action_indices) name = f"data_group_{action_indices_str}" # Ensure measuring callable matches the current action_indices @@ -733,7 +742,8 @@ def _measure_callable(self, callable, name=None, **kwargs): # No nested measurement has been performed in the callable. # Add results, which should be dict, by creating a nested measurement if not isinstance(results, dict): - raise SyntaxError(f"{name} results must be a dict, not {results}") + raise SyntaxError( + f"{name} results must be a dict, not {results}") with MeasurementLoop(name) as msmt: for key, val in results.items(): @@ -750,7 +760,8 @@ def _measure_dict(self, value: dict, name: str): raise SyntaxError(f"{name} must be a dict, not {value}") if not isinstance(name, str) or name == "": - raise SyntaxError(f"Dict result {name} must have a valid name: {value}") + raise SyntaxError( + f"Dict result {name} must have a valid name: {value}") # Ensure measuring callable matches the current action_indices self._verify_action(action=None, name=name, add_if_new=True) @@ -872,13 +883,15 @@ def measure( ) self.skip() # Increment last action index by 1 elif isinstance(measurable, MultiParameter): - result = self._measure_multi_parameter(measurable, name=name, **kwargs) + result = self._measure_multi_parameter( + measurable, name=name, **kwargs) elif callable(measurable): result = self._measure_callable(measurable, name=name, **kwargs) elif isinstance(measurable, dict): result = self._measure_dict(measurable, name=name) elif isinstance(measurable, RAW_VALUE_TYPES): - result = self._measure_value(measurable, name=name, label=label, unit=unit, **kwargs) + result = self._measure_value( + measurable, name=name, label=label, unit=unit, **kwargs) self.skip() # Increment last action index by 1 else: raise RuntimeError( @@ -894,7 +907,6 @@ def measure( 'T_post', unit='s', timestamp=False) self.skip() # Increment last action index by 1 - self.timings.record( ['measurement', initial_action_indices, 'total'], perf_counter() - t0 @@ -1155,7 +1167,8 @@ def step_out(self, reduce_dimension=True): This function usually doesn't need to be called. """ if MeasurementLoop.running_measurement is not self: - MeasurementLoop.running_measurement.step_out(reduce_dimension=reduce_dimension) + MeasurementLoop.running_measurement.step_out( + reduce_dimension=reduce_dimension) else: if reduce_dimension: self.loop_shape = self.loop_shape[:-1] @@ -1172,7 +1185,8 @@ def traceback(self): Measurement must be ran from separate thread """ if self.measurement_thread is None: - raise RuntimeError('Measurement was not started in separate thread') + raise RuntimeError( + 'Measurement was not started in separate thread') else: self.measurement_thread.traceback() @@ -1208,7 +1222,6 @@ def __next__(self): return value - class BaseSweep(AbstractSweep): """Sweep over an iterable inside a Measurement @@ -1232,11 +1245,13 @@ class BaseSweep(AbstractSweep): for param_val in Sweep(p. ``` """ + def __init__(self, sequence, name=None, label=None, unit=None, parameter=None, revert=False, delay=None, initial_delay=None): if isinstance(sequence, AbstractSweep): sequence = _IterateDondSweep(sequence) elif not isinstance(sequence, Iterable): - raise SyntaxError(f"Sweep sequence must be iterable, not {type(sequence)}") + raise SyntaxError( + f"Sweep sequence must be iterable, not {type(sequence)}") # Properties for the data array self.name = name @@ -1270,7 +1285,8 @@ def __repr__(self): components.append(f"'{self.name}'") # Add number of elements - num_elems = str(len(self.sequence)) if self.sequence is not None else 'unknown' + num_elems = str(len(self.sequence) + ) if self.sequence is not None else 'unknown' components.append(f'length={num_elems}') # Combine components @@ -1293,9 +1309,11 @@ def __iter__(self): if self.revert: if isinstance(self.sequence, SweepValues): - msmt.mask(self.sequence.parameter, self.sequence.parameter.get()) + msmt.mask(self.sequence.parameter, + self.sequence.parameter.get()) else: - raise NotImplementedError("Unable to revert non-parameter values.") + raise NotImplementedError( + "Unable to revert non-parameter values.") self.loop_index = 0 self.dimension = len(msmt.loop_shape) @@ -1441,11 +1459,13 @@ def execute( # Determine "name" if not provided from sweeps if name is None: dimensionality = 1 + len(sweeps) - sweep_names = [str(sweep.name) for sweep in sweeps] + [str(self.name)] + sweep_names = [str(sweep.name) + for sweep in sweeps] + [str(self.name)] name = f'{dimensionality}D_sweep_' + '_'.join(sweep_names) with MeasurementLoop(name) as msmt: - measure_sweeps(sweeps=sweeps, measure_params=measure_params, msmt=msmt) + measure_sweeps( + sweeps=sweeps, measure_params=measure_params, msmt=msmt) return msmt.dataset @@ -1476,11 +1496,11 @@ def post_actions(self) -> ActionsT: return [] - - class Sweep(BaseSweep): - sequence_keywords = ['start', 'stop', 'around', 'num', 'step', 'parameter', 'sequence'] - base_keywords = ['delay', 'initial_delay', 'name', 'label', 'unit', 'revert', 'parameter'] + sequence_keywords = ['start', 'stop', 'around', + 'num', 'step', 'parameter', 'sequence'] + base_keywords = ['delay', 'initial_delay', 'name', + 'label', 'unit', 'revert', 'parameter'] def __init__( self, @@ -1511,7 +1531,8 @@ def __init__( revert=revert ) - sequence_kwargs, base_kwargs = self._transform_args_to_kwargs(*args, **kwargs) + sequence_kwargs, base_kwargs = self._transform_args_to_kwargs( + *args, **kwargs) self._explicit_sequence = None self.sequence = self._generate_sequence(**sequence_kwargs) @@ -1548,14 +1569,16 @@ def _transform_args_to_kwargs(self, *args, **kwargs): """ if len(args) == 1: # Sweep([1,2,3], name='name') if isinstance(args[0], Iterable): - assert kwargs.get('name') is not None, "Must provide name if sweeping iterable" + assert kwargs.get( + 'name') is not None, "Must provide name if sweeping iterable" kwargs['sequence'], = args elif isinstance(args[0], _BaseParameter): assert kwargs.get('stop') is not None or kwargs.get('around') is not None, \ "Must provide stop value for parameter" kwargs['parameter'], = args else: - raise SyntaxError('Sweep with 1 arg must have iterable or parameter as arg') + raise SyntaxError( + 'Sweep with 1 arg must have iterable or parameter as arg') elif len(args) == 2: if isinstance(args[0], _BaseParameter): # Sweep(parameter, [1,2,3]) if isinstance(args[1], Iterable): @@ -1563,7 +1586,8 @@ def _transform_args_to_kwargs(self, *args, **kwargs): elif isinstance(args[1], (int, float)): kwargs['parameter'], kwargs['stop'] = args else: - raise SyntaxError('Sweep with Parameter arg and second arg should h') + raise SyntaxError( + 'Sweep with Parameter arg and second arg should h') elif isinstance(args[0], Iterable): # Sweep([1,2,3], 'name') assert isinstance(args[1], str) assert kwargs.get('name') is None @@ -1602,7 +1626,8 @@ def _transform_args_to_kwargs(self, *args, **kwargs): if kwargs.get(key) is None: kwargs[key] = val - sequence_kwargs = {key: kwargs.get(key) for key in self.sequence_keywords} + sequence_kwargs = {key: kwargs.get(key) + for key in self.sequence_keywords} base_kwargs = {key: kwargs.get(key) for key in self.base_keywords} return sequence_kwargs, base_kwargs @@ -1616,24 +1641,29 @@ def _generate_sequence(self, start=None, stop=None, around=None, num=None, step= # Verify that "around" is used with "parameter" but not with "start" and "stop" if around is not None: if start is not None or stop is not None: - raise SyntaxError('Cannot pass kwarg "around" and also "start" or "stop') + raise SyntaxError( + 'Cannot pass kwarg "around" and also "start" or "stop') elif parameter is None: - raise SyntaxError('Cannot use kwarg "around" without a parameter') + raise SyntaxError( + 'Cannot use kwarg "around" without a parameter') # Convert "around" to "start" and "stop" using parameter current value center_value = parameter() if center_value is None: - raise ValueError('Parameter must have initial value if "around" keyword is used') + raise ValueError( + 'Parameter must have initial value if "around" keyword is used') start = center_value - around stop = center_value + around elif stop is not None: # Use "parameter" current value if "start" is not provided if start is None: if parameter is None: - raise SyntaxError('Cannot use "stop" without "start" or a "parameter"') + raise SyntaxError( + 'Cannot use "stop" without "start" or a "parameter"') start = parameter() if start is None: - raise ValueError('Parameter must have initial value if start is not explicitly provided') + raise ValueError( + 'Parameter must have initial value if start is not explicitly provided') else: raise SyntaxError('Must provide either "around" or "stop"') @@ -1649,7 +1679,8 @@ def _generate_sequence(self, start=None, stop=None, around=None, num=None, step= if abs((stop - sequence[-1]) / step) > 1e-9: sequence = np.append(sequence, [stop]) else: - raise SyntaxError('Cannot determine measurement points. Either provide "sequence, "step" or "num"') + raise SyntaxError( + 'Cannot determine measurement points. Either provide "sequence, "step" or "num"') return sequence From c5180a5af17e77d42357361bf40d5760e84fc4e9 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 13 Sep 2022 15:36:30 +0200 Subject: [PATCH 050/122] Added type hints --- qcodes/dataset/measurement_loop.py | 245 ++++++++++++++++++----------- 1 file changed, 153 insertions(+), 92 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 230063fd133..f75baa89e92 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1,9 +1,10 @@ +from ast import Call import logging import threading import traceback from datetime import datetime from time import perf_counter, sleep -from typing import Any, Callable, Dict, Iterable, List, Sequence, Tuple, Union +from typing import Any, Callable, Dict, Iterable, List, Sequence, Tuple, Union, Optional import numpy as np @@ -13,7 +14,8 @@ from qcodes.dataset.descriptions.versioning import serialization as serial from qcodes.dataset.descriptions.versioning.converters import new_to_old from qcodes.dataset.do_nd import AbstractSweep -from qcodes.dataset.measurements import Measurement +from qcodes.dataset.measurements import Measurement, DataSaver, Runner +from qcodes.dataset.data_set_protocol import DataSetProtocol from qcodes.dataset.sqlite.queries import add_parameter, update_run_description from qcodes.instrument.base import InstrumentBase from qcodes.instrument.parameter import _BaseParameter, DelegateParameter, MultiParameter, Parameter @@ -27,7 +29,6 @@ get_last_input_cells, using_ipython, ) - RAW_VALUE_TYPES = (float, int, bool, np.ndarray, np.integer, np.floating, np.bool_, type(None)) @@ -35,24 +36,23 @@ class DatasetHandler: """Handler for a single DataSet (with Measurement and Runner)""" - def __init__(self, measurement_loop, name='results'): + def __init__(self, measurement_loop: "MeasurementLoop", name='results'): self.measurement_loop = measurement_loop self.name = name - self.initialized = False - self.datasaver = None - self.runner = None - self.measurement = None - self.dataset = None + self.initialized: bool = False + self.datasaver: DataSaver = None + self.runner: Runner = None + self.measurement: Measurement = None + self.dataset: DataSetProtocol = None # Key: action_index # Values: # - parameter # - dataset_parameter (differs from 'parameter' when multiple share same name) # - latest_value - self.setpoint_list = dict() + self.setpoint_list: Dict[Tuple[int], Any] = dict() - self.measurement_list = dict() # Dict with key being action_index and value is a dict containing # - parameter # - setpoints_action_indices @@ -60,6 +60,7 @@ def __init__(self, measurement_loop, name='results'): # - shape # - unstored_results - list where each element contains (*setpoints, measurement_value) # - latest_value + self.measurement_list: Dict[str, Any] = dict() self.initialize() @@ -82,7 +83,7 @@ def initialize(self): def finalize(self): self.datasaver.flush_data_to_database() - def _ensure_unique_parameter(self, parameter_info, setpoint, max_idx=99): + def _ensure_unique_parameter(self, parameter_info: dict, setpoint: bool, max_idx: int = 99): """Ensure parameters have unique names""" if setpoint: parameter_list = self.setpoint_list @@ -116,8 +117,12 @@ def _ensure_unique_parameter(self, parameter_info, setpoint, max_idx=99): parameter_info['dataset_parameter'] = delegate_parameter def create_measurement_info( - self, action_indices, parameter, name=None, label=None, unit=None - ): + self, action_indices: Tuple[int], + parameter: _BaseParameter, + name: Optional[str] = None, + label: Optional[str] = None, + unit: Optional[str] = None + ) -> Dict[str, Any]: if parameter is None: assert name is not None parameter = Parameter(name=name, label=label, unit=unit) @@ -151,11 +156,11 @@ def create_measurement_info( def register_new_measurement( self, - action_indices, - parameter, - name: str = None, - label: str = None, - unit: str = None + action_indices: Tuple[int], + parameter: _BaseParameter, + name: Optional[str] = None, + label: Optional[str] = None, + unit: Optional[str] = None ): measurement_info = self.create_measurement_info( action_indices=action_indices, @@ -171,12 +176,12 @@ def register_new_measurement( def add_measurement_result( self, - action_indices, - result, - parameter=None, - name: str = None, - label: str = None, - unit: str = None, + action_indices: Tuple[int], + result: Union[float, int, bool], + parameter: _BaseParameter = None, + name: Optional[str] = None, + label: Optional[str] = None, + unit: Optional[str] = None, ): """Store single measurement result @@ -342,12 +347,17 @@ class MeasurementLoop: # The last three are only not None if an error has occured notify_function = None - def __init__(self, name: str, force_cell_thread: bool = True, notify=False): - self.name = name + def __init__( + self, + name: Optional[str], + force_cell_thread: bool = True, + notify: bool = False + ): + self.name: str = name # Data handler is created during `with Measurement('name')` # Used to control dataset(s) - self.data_handler = None + self.data_handler: DataSaver = None # Total dimensionality of loop self.loop_shape: Union[Tuple[int], None] = None @@ -369,25 +379,27 @@ def __init__(self, name: str, force_cell_thread: bool = True, notify=False): self.is_paused: bool = False # Whether the Measurement is paused self.is_stopped: bool = False # Whether the Measurement is stopped - self.notify = notify + # Whether to notify upon measurement completion + self.notify: bool = notify - self.force_cell_thread = force_cell_thread and using_ipython() + # Whether to force measurement to start in new thread + self.force_cell_thread: bool = force_cell_thread and using_ipython() # Each measurement can have its own final actions, to be executed # regardless of whether the measurement finished successfully or not # Note that there are also Measurement.final_actions, which are always # executed when the outermost measurement finishes - self.final_actions = [] - self.except_actions = [] - self._masked_properties = [] + self.final_actions: List[Callable] = [] + self.except_actions: List[Callable] = [] + self._masked_properties: List[Dict[str, Any]] = [] - self.timings = PerformanceTimer() + self.timings: PerformanceTimer = PerformanceTimer() @property - def dataset(self): + def dataset(self) -> DataSetProtocol: return self.data_handler.dataset - def log(self, message: str, level="info"): + def log(self, message: str, level: str = "info"): """Send a log message Args: @@ -412,22 +424,22 @@ def data_groups(self) -> Dict[Tuple[int], "MeasurementLoop"]: return self._data_groups @property - def active_action(self): + def active_action(self) -> Optional[Tuple[int]]: return self.actions.get(self.action_indices, None) @property - def active_action_name(self): + def active_action_name(self) -> Optional[str]: return self.action_names.get(self.action_indices, None) @property - def setpoint_list(self): + def setpoint_list(self) -> Optional[Dict[Tuple[int], Any]]: if self.data_handler is not None: return self.data_handler.setpoint_list else: return None @property - def measurement_list(self): + def measurement_list(self) -> Optional[Dict[Tuple[int], Any]]: if self.data_handler is not None: return self.data_handler.measurement_list else: @@ -613,7 +625,7 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb): # } # ) - def _verify_action(self, action, name, add_if_new=True): + def _verify_action(self, action: Callable, name: str, add_if_new: bool = True): """Verify an action corresponds to the current action indices. This is only relevant if an action has previously been performed at @@ -647,7 +659,14 @@ def _apply_actions(self, actions: list, label="", clear=False): # Measurement-related functions # TODO these methods should always end up with a parameter - def _measure_parameter(self, parameter, name=None, label=None, unit=None, **kwargs): + def _measure_parameter( + self, + parameter: _BaseParameter, + name: Optional[str] = None, + label: Optional[str] = None, + unit: Optional[str] = None, + **kwargs + ) -> Any: """Measure parameter and store results. Called from `measure`. @@ -672,7 +691,12 @@ def _measure_parameter(self, parameter, name=None, label=None, unit=None, **kwar return result - def _measure_multi_parameter(self, multi_parameter, name=None, **kwargs): + def _measure_multi_parameter( + self, + multi_parameter: MultiParameter, + name: str = None, + **kwargs + ) -> Any: """Measure MultiParameter and store results Called from `measure` @@ -705,7 +729,7 @@ def _measure_multi_parameter(self, multi_parameter, name=None, **kwargs): return results - def _measure_callable(self, callable, name=None, **kwargs): + def _measure_callable(self, callable: Callable, name: str = None, **kwargs) -> Dict[str, Any]: """Measure a callable (function) and store results The function should return a dict, from which each item is measured. @@ -751,7 +775,7 @@ def _measure_callable(self, callable, name=None, **kwargs): return results - def _measure_dict(self, value: dict, name: str): + def _measure_dict(self, value: dict, name: str) -> Dict[str, Any]: """Store dictionary results Each key is an array name, and the value is the value to store @@ -772,7 +796,13 @@ def _measure_dict(self, value: dict, name: str): return value - def _measure_value(self, value, name, parameter=None, label=None, unit=None): + def _measure_value( + self, + value: Union[float, int, bool], + name: str, + parameter: Optional[_BaseParameter] = None, + label: Optional[str] = None, + unit: Optional[str] = None) -> Union[float, int, bool]: """Store a single value (float/int/bool) If this value comes from another parameter acquisition, e.g. from a @@ -807,11 +837,11 @@ def measure( measurable: Union[ Parameter, Callable, dict, float, int, bool, np.ndarray, None ], - name=None, + name: Optional[str] = None, *, # Everything after here must be a kwarg - label=None, - unit=None, - timestamp=False, + label: Optional[str] = None, + unit: Optional[str] = None, + timestamp: bool = False, **kwargs, ): """Perform a single measurement of a Parameter, function, etc. @@ -915,7 +945,7 @@ def measure( return result # Methods related to masking of parameters/attributes/keys - def _mask_attr(self, obj: object, attr: str, value): + def _mask_attr(self, obj: object, attr: str, value) -> Any: """Temporarily override an object attribute during the measurement. The value will be reset at the end of the measurement @@ -944,7 +974,7 @@ def _mask_attr(self, obj: object, attr: str, value): return original_value - def _mask_parameter(self, param, value): + def _mask_parameter(self, param: _BaseParameter, value: Any) -> Any: """Temporarily override a parameter value during the measurement. The value will be reset at the end of the measurement. @@ -971,7 +1001,7 @@ def _mask_parameter(self, param, value): return original_value - def _mask_key(self, obj: dict, key: str, value): + def _mask_key(self, obj: dict, key: str, value: Any) -> Any: """Temporarily override a dictionary key during the measurement. The value will be reset at the end of the measurement @@ -1000,7 +1030,7 @@ def _mask_key(self, obj: dict, key: str, value): return original_value - def mask(self, obj: Union[object, dict], val=None, **kwargs): + def mask(self, obj: Union[object, dict], val: Any = None, **kwargs) -> Any: """Mask a key/attribute/parameter for the duration of the Measurement Multiple properties can be masked by passing as kwargs. @@ -1017,7 +1047,7 @@ def mask(self, obj: Union[object, dict], val=None, **kwargs): **kwargs: Masked properties Returns: - List of original values before masking + List of original values before masking, or single value if parameter is passed Examples: ``` @@ -1055,12 +1085,12 @@ def mask(self, obj: Union[object, dict], val=None, **kwargs): def unmask( self, - obj, - attr=None, - key=None, + obj: Union[object, dict], + attr: Optional[str] = None, + key: Optional[str] = None, type=None, - value=None, - raise_exception=True, + value: Optional[Any] = None, + raise_exception: bool = True, **kwargs # Add kwargs because original_value may be None ): if 'original_value' not in kwargs: @@ -1127,13 +1157,16 @@ def stop(self): # Unpause loop running_measurement().resume() - def skip(self, N=1): + def skip(self, N: int = 1) -> Tuple[int]: """Skip an action index. Useful if a measure is only sometimes run Args: N: number of action indices to skip + + Returns: + Measurement action_indices after skipping Examples: This measurement repeatedly creates a random value. @@ -1161,7 +1194,7 @@ def skip(self, N=1): self.action_indices = tuple(action_indices) return self.action_indices - def step_out(self, reduce_dimension=True): + def step_out(self, reduce_dimension: bool = True): """Step out of a Sweep This function usually doesn't need to be called. @@ -1198,14 +1231,14 @@ def running_measurement() -> MeasurementLoop: class _IterateDondSweep: def __init__(self, sweep: AbstractSweep): - self.sweep = sweep - self.iterator = None - self.parameter = sweep._param + self.sweep: AbstractSweep = sweep + self.iterator: Iterable = None + self.parameter: _BaseParameter = sweep._param - def __len__(self): + def __len__(self) -> int: return self.sweep.num_points - def __iter__(self): + def __iter__(self) -> Iterable: self.iterator = iter(self.sweep.get_setpoints()) return self @@ -1246,7 +1279,17 @@ class BaseSweep(AbstractSweep): ``` """ - def __init__(self, sequence, name=None, label=None, unit=None, parameter=None, revert=False, delay=None, initial_delay=None): + def __init__( + self, + sequence: Union[Iterable, SweepValues, AbstractSweep], + name: Optional[str] = None, + label: Optional[str] = None, + unit: Optional[str] = None, + parameter: Optional[_BaseParameter] = None, + revert: bool = False, + delay: Optional[float] = None, + initial_delay: Optional[float] = None + ): if isinstance(sequence, AbstractSweep): sequence = _IterateDondSweep(sequence) elif not isinstance(sequence, Iterable): @@ -1254,28 +1297,28 @@ def __init__(self, sequence, name=None, label=None, unit=None, parameter=None, r f"Sweep sequence must be iterable, not {type(sequence)}") # Properties for the data array - self.name = name - self.label = label - self.unit = unit - self.parameter = parameter - - self.sequence = sequence - self.dimension = None - self.loop_index = None - self.iterator = None - self.revert = revert - self._delay = delay - self.initial_delay = initial_delay + self.name: Optional[str] = name + self.label: Optional[str] = label + self.unit: Optional[str] = unit + self.parameter: _BaseParameter = parameter + + self.sequence: Union[Iterable, SweepValues, AbstractSweep] = sequence + self.dimension: Optional[int] = None + self.loop_index: Optional[Tuple[int]] = None + self.iterator: Optional[Iterable] = None + self.revert: bool = revert + self._delay: Optional[float] = delay + self.initial_delay: Optional[float] = initial_delay # setpoint_info will be populated once the sweep starts - self.setpoint_info = None + self.setpoint_info: Optional[Dict[str, Any]] = None # Validate values if self.parameter is not None and hasattr(self.parameter, 'validate'): for value in self.sequence: self.parameter.validate(value) - def __repr__(self): + def __repr__(self) -> str: components = [] # Add parameter or name @@ -1293,10 +1336,10 @@ def __repr__(self): components_str = ', '.join(components) return f'Sweep({components_str})' - def __len__(self): + def __len__(self) -> int: return len(self.sequence) - def __iter__(self): + def __iter__(self) -> Iterable: if threading.current_thread() is not MeasurementLoop.measurement_thread: raise RuntimeError( "Cannot create a Sweep while another measurement " @@ -1328,7 +1371,7 @@ def __iter__(self): return self - def __next__(self): + def __next__(self) -> Any: msmt = running_measurement() if not msmt.is_context_manager: @@ -1379,7 +1422,7 @@ def __next__(self): return sweep_value - def initialize(self): + def initialize(self) -> Dict[str, Any]: msmt = running_measurement() if msmt.action_indices in msmt.setpoint_list: return msmt.setpoint_list[msmt.action_indices] @@ -1424,7 +1467,7 @@ def execute( measure_params: Union[Iterable, _BaseParameter] = None, repetitions: int = 1, sweep: Union[Iterable, 'BaseSweep'] = None - ): + ) -> DataSetProtocol: # Get "measure_params" from station if not provided if measure_params is None: station = Station.default @@ -1534,8 +1577,7 @@ def __init__( sequence_kwargs, base_kwargs = self._transform_args_to_kwargs( *args, **kwargs) - self._explicit_sequence = None - self.sequence = self._generate_sequence(**sequence_kwargs) + self.sequence: Iterable = self._generate_sequence(**sequence_kwargs) super().__init__(sequence=self.sequence, **base_kwargs) @@ -1632,7 +1674,16 @@ def _transform_args_to_kwargs(self, *args, **kwargs): return sequence_kwargs, base_kwargs - def _generate_sequence(self, start=None, stop=None, around=None, num=None, step=None, parameter=None, sequence=None): + def _generate_sequence( + self, + start: Optional[float] = None, + stop: Optional[float] = None, + around: Optional[float] = None, + num: Optional[int] = None, + step: Optional[float] = None, + parameter: Optional[_BaseParameter] = None, + sequence: Optional[Iterable] = None + ): """Creates a sequence from passed values""" # Return "sequence" if explicitly provided if sequence is not None: @@ -1686,7 +1737,13 @@ def _generate_sequence(self, start=None, stop=None, around=None, num=None, step= class RepetitionSweep(BaseSweep): - def __init__(self, repetitions, start=0, name='repetition', label='Repetition', unit=None): + def __init__( + self, + repetitions: int, + start: int = 0, + name: str = 'repetition', + label: str = 'Repetition', + unit: Optional[str] = None): self.start = start self.repetitions = repetitions sequence = start + np.arange(repetitions) @@ -1694,7 +1751,11 @@ def __init__(self, repetitions, start=0, name='repetition', label='Repetition', super().__init__(sequence, name, label, unit) -def measure_sweeps(sweeps: list[BaseSweep], measure_params: list[_BaseParameter], msmt: MeasurementLoop = None): +def measure_sweeps( + sweeps: list[BaseSweep], + measure_params: list[_BaseParameter], + msmt: MeasurementLoop = None +): """Recursively iterate over Sweep objects, measuring measure_params in innermost loop Args: From 79c5cc952082eb7dcff0e172c703ec4271f5d840 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 13 Sep 2022 15:42:32 +0200 Subject: [PATCH 051/122] fix public api imports --- qcodes/dataset/measurement_loop.py | 18 +++++++++++------- .../test_measurement_loop_basics.py | 16 ++++++++++------ .../test_measurement_loop_sweep.py | 16 ++++++++++------ 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index f75baa89e92..0acfd99ba82 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -9,18 +9,22 @@ import numpy as np from qcodes import config as qcodes_config +from qcodes.dataset import AbstractSweep, Measurement, DataSetProtocol from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement from qcodes.dataset.descriptions.rundescriber import RunDescriber from qcodes.dataset.descriptions.versioning import serialization as serial from qcodes.dataset.descriptions.versioning.converters import new_to_old -from qcodes.dataset.do_nd import AbstractSweep -from qcodes.dataset.measurements import Measurement, DataSaver, Runner -from qcodes.dataset.data_set_protocol import DataSetProtocol +from qcodes.dataset.measurements import DataSaver, Runner from qcodes.dataset.sqlite.queries import add_parameter, update_run_description -from qcodes.instrument.base import InstrumentBase -from qcodes.instrument.parameter import _BaseParameter, DelegateParameter, MultiParameter, Parameter -from qcodes.instrument.sweep_values import SweepValues -from qcodes.parameters.parameter_base import ParameterBase + +from qcodes.instrument import ( + InstrumentBase, + DelegateParameter, + MultiParameter, + Parameter, + SweepValues +) +from qcodes.parameters import ParameterBase from qcodes.station import Station from qcodes.utils.dataset.doNd import AbstractSweep, ActionsT from qcodes.utils.helpers import ( diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 5742f4a4786..b0069642940 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -2,15 +2,19 @@ import shutil import tempfile from pathlib import Path - import numpy as np import pytest -from qcodes import ManualParameter, Parameter -from qcodes.dataset import initialise_or_create_database_at, load_or_create_experiment -from qcodes.dataset.data_set import load_by_id -from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep -from qcodes.utils.dataset.doNd import LinSweep +from qcodes.instrument import ManualParameter, Parameter +from qcodes.dataset import ( + initialise_or_create_database_at, + load_or_create_experiment, + load_by_id, + MeasurementLoop, + Sweep, + dond, + LinSweep +) @pytest.mark.usefixtures("empty_temp_db", "experiment") diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index 7037432c6a2..72478e468f8 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -2,15 +2,19 @@ import shutil import tempfile from pathlib import Path - import numpy as np import pytest -from qcodes import ManualParameter, Parameter -from qcodes.dataset import initialise_or_create_database_at, load_or_create_experiment -from qcodes.dataset.data_set import load_by_id -from qcodes.dataset.measurement_loop import MeasurementLoop, Sweep -from qcodes.utils.dataset.doNd import LinSweep, dond +from qcodes.instrument import ManualParameter, Parameter +from qcodes.dataset import ( + initialise_or_create_database_at, + load_or_create_experiment, + load_by_id, + MeasurementLoop, + Sweep, + dond, + LinSweep +) def test_sweep_1_arg_sequence(): From ea1be0fa33a7347066ed709f4f210ffd902c32f2 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 13 Sep 2022 15:44:30 +0200 Subject: [PATCH 052/122] attempting fix of keysight __init__.py --- qcodes/instrument_drivers/Keysight/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 qcodes/instrument_drivers/Keysight/__init__.py diff --git a/qcodes/instrument_drivers/Keysight/__init__.py b/qcodes/instrument_drivers/Keysight/__init__.py new file mode 100644 index 00000000000..cfa2d9632f9 --- /dev/null +++ b/qcodes/instrument_drivers/Keysight/__init__.py @@ -0,0 +1 @@ +# Intentionally left blank \ No newline at end of file From 513ca144e5d668d52a58c17f9d3a83323c82eb68 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 13 Sep 2022 15:44:56 +0200 Subject: [PATCH 053/122] added newline after keysight/__init__.py --- qcodes/instrument_drivers/Keysight/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/Keysight/__init__.py b/qcodes/instrument_drivers/Keysight/__init__.py index cfa2d9632f9..e484f8b84cd 100644 --- a/qcodes/instrument_drivers/Keysight/__init__.py +++ b/qcodes/instrument_drivers/Keysight/__init__.py @@ -1 +1 @@ -# Intentionally left blank \ No newline at end of file +# Intentionally left blank From e05a82eb60b8df1ecfb01745e47e6359e037ff9e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Sep 2022 13:45:43 +0000 Subject: [PATCH 054/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 94 +++++++++---------- .../test_measurement_loop_basics.py | 15 +-- .../test_measurement_loop_sweep.py | 15 +-- qcodes/utils/helpers.py | 6 +- 4 files changed, 65 insertions(+), 65 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 0acfd99ba82..1bcf411173a 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1,28 +1,27 @@ -from ast import Call import logging import threading import traceback +from ast import Call from datetime import datetime from time import perf_counter, sleep -from typing import Any, Callable, Dict, Iterable, List, Sequence, Tuple, Union, Optional +from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union import numpy as np from qcodes import config as qcodes_config -from qcodes.dataset import AbstractSweep, Measurement, DataSetProtocol +from qcodes.dataset import AbstractSweep, DataSetProtocol, Measurement from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement from qcodes.dataset.descriptions.rundescriber import RunDescriber from qcodes.dataset.descriptions.versioning import serialization as serial from qcodes.dataset.descriptions.versioning.converters import new_to_old from qcodes.dataset.measurements import DataSaver, Runner from qcodes.dataset.sqlite.queries import add_parameter, update_run_description - from qcodes.instrument import ( - InstrumentBase, - DelegateParameter, - MultiParameter, - Parameter, - SweepValues + DelegateParameter, + InstrumentBase, + MultiParameter, + Parameter, + SweepValues, ) from qcodes.parameters import ParameterBase from qcodes.station import Station @@ -33,6 +32,7 @@ get_last_input_cells, using_ipython, ) + RAW_VALUE_TYPES = (float, int, bool, np.ndarray, np.integer, np.floating, np.bool_, type(None)) @@ -121,10 +121,11 @@ def _ensure_unique_parameter(self, parameter_info: dict, setpoint: bool, max_idx parameter_info['dataset_parameter'] = delegate_parameter def create_measurement_info( - self, action_indices: Tuple[int], - parameter: _BaseParameter, - name: Optional[str] = None, - label: Optional[str] = None, + self, + action_indices: Tuple[int], + parameter: _BaseParameter, + name: Optional[str] = None, + label: Optional[str] = None, unit: Optional[str] = None ) -> Dict[str, Any]: if parameter is None: @@ -352,10 +353,7 @@ class MeasurementLoop: notify_function = None def __init__( - self, - name: Optional[str], - force_cell_thread: bool = True, - notify: bool = False + self, name: Optional[str], force_cell_thread: bool = True, notify: bool = False ): self.name: str = name @@ -664,11 +662,11 @@ def _apply_actions(self, actions: list, label="", clear=False): # Measurement-related functions # TODO these methods should always end up with a parameter def _measure_parameter( - self, - parameter: _BaseParameter, - name: Optional[str] = None, - label: Optional[str] = None, - unit: Optional[str] = None, + self, + parameter: _BaseParameter, + name: Optional[str] = None, + label: Optional[str] = None, + unit: Optional[str] = None, **kwargs ) -> Any: """Measure parameter and store results. @@ -696,10 +694,7 @@ def _measure_parameter( return result def _measure_multi_parameter( - self, - multi_parameter: MultiParameter, - name: str = None, - **kwargs + self, multi_parameter: MultiParameter, name: str = None, **kwargs ) -> Any: """Measure MultiParameter and store results @@ -801,11 +796,11 @@ def _measure_dict(self, value: dict, name: str) -> Dict[str, Any]: return value def _measure_value( - self, - value: Union[float, int, bool], - name: str, - parameter: Optional[_BaseParameter] = None, - label: Optional[str] = None, + self, + value: Union[float, int, bool], + name: str, + parameter: Optional[_BaseParameter] = None, + label: Optional[str] = None, unit: Optional[str] = None) -> Union[float, int, bool]: """Store a single value (float/int/bool) @@ -1168,7 +1163,7 @@ def skip(self, N: int = 1) -> Tuple[int]: Args: N: number of action indices to skip - + Returns: Measurement action_indices after skipping @@ -1284,8 +1279,8 @@ class BaseSweep(AbstractSweep): """ def __init__( - self, - sequence: Union[Iterable, SweepValues, AbstractSweep], + self, + sequence: Union[Iterable, SweepValues, AbstractSweep], name: Optional[str] = None, label: Optional[str] = None, unit: Optional[str] = None, @@ -1679,13 +1674,13 @@ def _transform_args_to_kwargs(self, *args, **kwargs): return sequence_kwargs, base_kwargs def _generate_sequence( - self, - start: Optional[float] = None, - stop: Optional[float] = None, - around: Optional[float] = None, - num: Optional[int] = None, - step: Optional[float] = None, - parameter: Optional[_BaseParameter] = None, + self, + start: Optional[float] = None, + stop: Optional[float] = None, + around: Optional[float] = None, + num: Optional[int] = None, + step: Optional[float] = None, + parameter: Optional[_BaseParameter] = None, sequence: Optional[Iterable] = None ): """Creates a sequence from passed values""" @@ -1742,12 +1737,13 @@ def _generate_sequence( class RepetitionSweep(BaseSweep): def __init__( - self, - repetitions: int, - start: int = 0, - name: str = 'repetition', - label: str = 'Repetition', - unit: Optional[str] = None): + self, + repetitions: int, + start: int = 0, + name: str = "repetition", + label: str = "Repetition", + unit: Optional[str] = None, + ): self.start = start self.repetitions = repetitions sequence = start + np.arange(repetitions) @@ -1756,8 +1752,8 @@ def __init__( def measure_sweeps( - sweeps: list[BaseSweep], - measure_params: list[_BaseParameter], + sweeps: list[BaseSweep], + measure_params: list[_BaseParameter], msmt: MeasurementLoop = None ): """Recursively iterate over Sweep objects, measuring measure_params in innermost loop diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index b0069642940..52a11e4eb4c 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -2,19 +2,20 @@ import shutil import tempfile from pathlib import Path + import numpy as np import pytest -from qcodes.instrument import ManualParameter, Parameter from qcodes.dataset import ( - initialise_or_create_database_at, - load_or_create_experiment, - load_by_id, - MeasurementLoop, + LinSweep, + MeasurementLoop, Sweep, - dond, - LinSweep + dond, + initialise_or_create_database_at, + load_by_id, + load_or_create_experiment, ) +from qcodes.instrument import ManualParameter, Parameter @pytest.mark.usefixtures("empty_temp_db", "experiment") diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index 72478e468f8..35edea40a35 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -2,19 +2,20 @@ import shutil import tempfile from pathlib import Path + import numpy as np import pytest -from qcodes.instrument import ManualParameter, Parameter from qcodes.dataset import ( - initialise_or_create_database_at, - load_or_create_experiment, - load_by_id, - MeasurementLoop, + LinSweep, + MeasurementLoop, Sweep, - dond, - LinSweep + dond, + initialise_or_create_database_at, + load_by_id, + load_or_create_experiment, ) +from qcodes.instrument import ManualParameter, Parameter def test_sweep_1_arg_sequence(): diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index 54b89a5095c..bc592089672 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -2,8 +2,8 @@ Module left for backwards compatibility. Please do not import from this in any new code """ -from contextlib import contextmanager import logging +from contextlib import contextmanager # for backwards compatibility since this module used # to contain logic that would abstract between yaml @@ -46,8 +46,10 @@ def warn_units(class_name: str, instance: object) -> None: import builtins import sys import time -import numpy as np from pprint import pprint + +import numpy as np + from qcodes.configuration.config import DotDict From fa8565785586a3444cf2ac631e5528dd9252ab3d Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 13 Sep 2022 15:52:22 +0200 Subject: [PATCH 055/122] added _BaseParameter import --- qcodes/dataset/measurement_loop.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 0acfd99ba82..57ce979c74e 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -24,6 +24,7 @@ Parameter, SweepValues ) +from qcodes.instrument.parameter import _BaseParameter from qcodes.parameters import ParameterBase from qcodes.station import Station from qcodes.utils.dataset.doNd import AbstractSweep, ActionsT From 1fcab47225486d3f156d58801444d37be512a176 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 13 Sep 2022 16:01:37 +0200 Subject: [PATCH 056/122] Changed to double-quotes --- qcodes/dataset/measurement_loop.py | 248 ++++++++++++++--------------- 1 file changed, 124 insertions(+), 124 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 04ee6c071fd..815424d6ae1 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -41,7 +41,7 @@ class DatasetHandler: """Handler for a single DataSet (with Measurement and Runner)""" - def __init__(self, measurement_loop: "MeasurementLoop", name='results'): + def __init__(self, measurement_loop: "MeasurementLoop", name="results"): self.measurement_loop = measurement_loop self.name = name @@ -54,7 +54,7 @@ def __init__(self, measurement_loop: "MeasurementLoop", name='results'): # Key: action_index # Values: # - parameter - # - dataset_parameter (differs from 'parameter' when multiple share same name) + # - dataset_parameter (differs from "parameter" when multiple share same name) # - latest_value self.setpoint_list: Dict[Tuple[int], Any] = dict() @@ -96,30 +96,30 @@ def _ensure_unique_parameter(self, parameter_info: dict, setpoint: bool, max_idx parameter_list = self.measurement_list parameter_names = [ - param_info['dataset_parameter'].name + param_info["dataset_parameter"].name for param_info in parameter_list.values() - if 'dataset_parameter' in param_info + if "dataset_parameter" in param_info ] - parameter_name = parameter_info['parameter'].name + parameter_name = parameter_info["parameter"].name if parameter_name not in parameter_names: - parameter_info['dataset_parameter'] = parameter_info['parameter'] + parameter_info["dataset_parameter"] = parameter_info["parameter"] else: for idx in range(1, max_idx): - parameter_idx_name = f'{parameter_name}_{idx}' + parameter_idx_name = f"{parameter_name}_{idx}" if parameter_idx_name not in parameter_names: parameter_name = parameter_idx_name break else: raise OverflowError( - f'All parameter names {parameter_name}_{{idx}} up to idx {max_idx} are taken' + f"All parameter names {parameter_name}_{{idx}} up to idx {max_idx} are taken" ) # Create a delegate parameter with modified name delegate_parameter = DelegateParameter( name=parameter_name, - source=parameter_info['parameter'] + source=parameter_info["parameter"] ) - parameter_info['dataset_parameter'] = delegate_parameter + parameter_info["dataset_parameter"] = delegate_parameter def create_measurement_info( self, @@ -134,9 +134,9 @@ def create_measurement_info( parameter = Parameter(name=name, label=label, unit=unit) elif {name, label, unit} != {None, }: overwrite_attrs = { - 'name': name, - 'label': label, - 'unit': unit + "name": name, + "label": label, + "unit": unit } overwrite_attrs = {key: val for key, val in overwrite_attrs.items() if val is not None} @@ -151,11 +151,11 @@ def create_measurement_info( setpoints_action_indices.append(action_indices[:k]) measurement_info = { - 'parameter': parameter, - 'setpoints_action_indices': setpoints_action_indices, - 'shape': self.measurement_loop.loop_shape, - 'unstored_results': [], - 'registered': False + "parameter": parameter, + "setpoints_action_indices": setpoints_action_indices, + "shape": self.measurement_loop.loop_shape, + "unstored_results": [], + "registered": False } return measurement_info @@ -214,16 +214,16 @@ def add_measurement_result( if name is None and parameter is not None: name = parameter.name - if name != measurement_info['parameter'].name: + if name != measurement_info["parameter"].name: raise SyntaxError( - f'Provided name {name} must match that of previous measurement ' + f"Provided name {name} must match that of previous measurement " f"{measurement_info['parameter'].name}" ) # Store result setpoints = [ - self.setpoint_list[action_indices]['latest_value'] - for action_indices in measurement_info['setpoints_action_indices'] + self.setpoint_list[action_indices]["latest_value"] + for action_indices in measurement_info["setpoints_action_indices"] ] parameters = ( *measurement_info["setpoint_parameters"], @@ -244,34 +244,34 @@ def _update_interdependencies(self): # Register all new setpoints parameters in Measurement for setpoint_info in self.setpoint_list.values(): - if setpoint_info['registered']: + if setpoint_info["registered"]: # Already registered continue self._ensure_unique_parameter(setpoint_info, setpoint=True) self.measurement.register_parameter( - setpoint_info['dataset_parameter']) - setpoint_info['registered'] = True + setpoint_info["dataset_parameter"]) + setpoint_info["registered"] = True # Register all measurement parameters in Measurement for measurement_info in self.measurement_list.values(): - if measurement_info['registered']: + if measurement_info["registered"]: # Already registered continue # Determine setpoint_parameters for each measurement_parameter for measurement_info in self.measurement_list.values(): - measurement_info['setpoint_parameters'] = tuple( - self.setpoint_list[action_indices]['dataset_parameter'] - for action_indices in measurement_info['setpoints_action_indices'] + measurement_info["setpoint_parameters"] = tuple( + self.setpoint_list[action_indices]["dataset_parameter"] + for action_indices in measurement_info["setpoints_action_indices"] ) self._ensure_unique_parameter(measurement_info, setpoint=False) self.measurement.register_parameter( - measurement_info['dataset_parameter'], - setpoints=measurement_info['setpoint_parameters'] + measurement_info["dataset_parameter"], + setpoints=measurement_info["setpoint_parameters"] ) - measurement_info['registered'] = True + measurement_info["registered"] = True self.measurement.set_shapes( detect_shape_of_measurement( (measurement_info["dataset_parameter"], @@ -328,8 +328,8 @@ class MeasurementLoop: Notes: When the Measurement is started in a separate thread (using %%new_job), - the Measurement is registered in the user namespace as 'msmt', and the - dataset as 'data' + the Measurement is registered in the user namespace as "msmt", and the + dataset as "data" """ @@ -358,7 +358,7 @@ def __init__( ): self.name: str = name - # Data handler is created during `with Measurement('name')` + # Data handler is created during `with Measurement("name")` # Used to control dataset(s) self.data_handler: DataSaver = None @@ -468,10 +468,10 @@ def __enter__(self): # TODO incorporate metadata # self._initialize_metadata(self.dataset) - # with self.timings.record(['dataset', 'save_metadata']): + # with self.timings.record(["dataset", "save_metadata"]): # self.dataset.save_metadata() - # if hasattr(self.dataset, 'save_config'): + # if hasattr(self.dataset, "save_config"): # self.dataset.save_config() # Initialize attributes @@ -481,8 +481,8 @@ def __enter__(self): self.data_arrays = {} self.set_arrays = {} - # self.log(f'Measurement started {self.dataset.location}') - # print(f'Measurement started {self.dataset.location}') + # self.log(f"Measurement started {self.dataset.location}") + # print(f"Measurement started {self.dataset.location}") else: if threading.current_thread() is not MeasurementLoop.measurement_thread: @@ -496,10 +496,10 @@ def __enter__(self): msmt = MeasurementLoop.running_measurement msmt.data_groups[msmt.action_indices] = self data_groups = [ - (key, getattr(val, 'name', 'None')) for key, val in msmt.data_groups.items() + (key, getattr(val, "name", "None")) for key, val in msmt.data_groups.items() ] # TODO add metadata - # msmt.dataset.add_metadata({'data_groups': data_groups}) + # msmt.dataset.add_metadata({"data_groups": data_groups}) msmt.action_indices += (0,) # Nested measurement attributes should mimic the primary measurement @@ -522,7 +522,7 @@ def __enter__(self): ) # Register the Measurement and data as variables in the user namespace - # Usually as variable names are 'msmt' and 'data' respectively + # Usually as variable names are "msmt" and "data" respectively from IPython import get_ipython shell = get_ipython() @@ -579,14 +579,14 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb): except: self.log("Could not notify", level="error") - t_stop = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + t_stop = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # TODO include metadata # self.data_handler.add_metadata({"t_stop": t_stop}) # self.data_handler.add_metadata({"timings": self.timings}) self.data_handler.finalize() - self.log(f'Measurement finished') + self.log(f"Measurement finished") else: msmt.step_out(reduce_dimension=False) @@ -614,7 +614,7 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb): # measurement_code = measurement_cell # # If the code is run from a measurement thread, there is some # # initial code that should be stripped - # init_string = "get_ipython().run_cell_magic('new_job', '', " + # init_string = "get_ipython().run_cell_magic("new_job", ", " # if measurement_code.startswith(init_string): # measurement_code = measurement_code[len(init_string) + 1 : -4] @@ -624,7 +624,7 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb): # "measurement_cell": measurement_cell, # "measurement_code": measurement_code, # "last_input_cells": get_last_input_cells(20), - # "t_start": self._t_start.strftime('%Y-%m-%d %H:%M:%S') + # "t_start": self._t_start.strftime("%Y-%m-%d %H:%M:%S") # } # ) @@ -709,7 +709,7 @@ def _measure_multi_parameter( # Ensure measuring multi_parameter matches the current action_indices self._verify_action(action=multi_parameter, name=name, add_if_new=True) - with self.timings.record(['measurement', self.action_indices, 'get']): + with self.timings.record(["measurement", self.action_indices, "get"]): results_list = multi_parameter(**kwargs) results = dict(zip(multi_parameter.names, results_list)) @@ -734,7 +734,7 @@ def _measure_callable(self, callable: Callable, name: str = None, **kwargs) -> D The function should return a dict, from which each item is measured. If the function already contains creates a Measurement, the return - values aren't stored. + values aren"t stored. """ # Determine name if name is None: @@ -903,7 +903,7 @@ def measure( # Store time referenced to t_start self.measure((t_now - self._t_start).total_seconds(), - 'T_pre', unit='s', timestamp=False) + "T_pre", unit="s", timestamp=False) self.skip() # Increment last action index by 1 # TODO Incorporate kwargs name, label, and unit, into each of these @@ -934,11 +934,11 @@ def measure( # Store time referenced to t_start self.measure((t_now - self._t_start).total_seconds(), - 'T_post', unit='s', timestamp=False) + "T_post", unit="s", timestamp=False) self.skip() # Increment last action index by 1 self.timings.record( - ['measurement', initial_action_indices, 'total'], + ["measurement", initial_action_indices, "total"], perf_counter() - t0 ) @@ -1054,7 +1054,7 @@ def mask(self, obj: Union[object, dict], val: Any = None, **kwargs) -> Any: node = ParameterNode() node.p1 = Parameter(initial_value=1, set_cmd=None) - with Measurement('test_masking') as msmt: + with Measurement("test_masking") as msmt: msmt.mask(node, p1=2) print(f"node.p1 has value {node.p1}") >>> node.p1 has value 2 @@ -1093,7 +1093,7 @@ def unmask( raise_exception: bool = True, **kwargs # Add kwargs because original_value may be None ): - if 'original_value' not in kwargs: + if "original_value" not in kwargs: # No masked property passed. We collect all the masked properties # that satisfy these requirements and unmask each of them. unmask_properties = [] @@ -1115,7 +1115,7 @@ def unmask( else: # A masked property has been passed, which we unmask here try: - original_value = kwargs['original_value'] + original_value = kwargs["original_value"] if type == "key": obj[key] = original_value elif type == "attr": @@ -1175,15 +1175,15 @@ def skip(self, N: int = 1) -> Tuple[int]: value is not above this threshold, the second measurement would become the first measurement if msmt.skip is not called ``` - with Measurement('skip_measurement') as msmt: + with Measurement("skip_measurement") as msmt: for k in Sweep(range(10)): random_value = np.random.rand() if random_value > 0.7: - msmt.measure(random_value, 'random_value_conditional') + msmt.measure(random_value, "random_value_conditional") else: msmt.skip() - msmt.measure(random_value, 'random_value_unconditional) + msmt.measure(random_value, "random_value_unconditional) ``` """ if running_measurement() is not self: @@ -1197,7 +1197,7 @@ def skip(self, N: int = 1) -> Tuple[int]: def step_out(self, reduce_dimension: bool = True): """Step out of a Sweep - This function usually doesn't need to be called. + This function usually doesn"t need to be called. """ if MeasurementLoop.running_measurement is not self: MeasurementLoop.running_measurement.step_out( @@ -1219,7 +1219,7 @@ def traceback(self): """ if self.measurement_thread is None: raise RuntimeError( - 'Measurement was not started in separate thread') + "Measurement was not started in separate thread") else: self.measurement_thread.traceback() @@ -1270,11 +1270,11 @@ class BaseSweep(AbstractSweep): Examples: ``` - with Measurement('sweep_msmt') as msmt: - for value in Sweep(np.linspace(5), 'sweep_values'): - msmt.measure(value, 'linearly_increasing_value') + with Measurement("sweep_msmt") as msmt: + for value in Sweep(np.linspace(5), "sweep_values"): + msmt.measure(value, "linearly_increasing_value") - p = Parameter('my_parameter') + p = Parameter("my_parameter") for param_val in Sweep(p. ``` """ @@ -1314,7 +1314,7 @@ def __init__( self.setpoint_info: Optional[Dict[str, Any]] = None # Validate values - if self.parameter is not None and hasattr(self.parameter, 'validate'): + if self.parameter is not None and hasattr(self.parameter, "validate"): for value in self.sequence: self.parameter.validate(value) @@ -1323,18 +1323,18 @@ def __repr__(self) -> str: # Add parameter or name if self.parameter is not None: - components.append(f'parameter={self.parameter}') + components.append(f"parameter={self.parameter}") elif self.name is not None: - components.append(f"'{self.name}'") + components.append(f"{self.name}") # Add number of elements num_elems = str(len(self.sequence) - ) if self.sequence is not None else 'unknown' - components.append(f'length={num_elems}') + ) if self.sequence is not None else "unknown" + components.append(f"length={num_elems}") # Combine components - components_str = ', '.join(components) - return f'Sweep({components_str})' + components_str = ", ".join(components) + return f"Sweep({components_str})" def __len__(self) -> int: return len(self.sequence) @@ -1416,7 +1416,7 @@ def __next__(self) -> Any: if self.delay: sleep(self.delay) - self.setpoint_info['latest_value'] = sweep_value + self.setpoint_info["latest_value"] = sweep_value self.loop_index += 1 @@ -1441,9 +1441,9 @@ def initialize(self) -> Dict[str, Any]: ) setpoint_info = { - 'parameter': self.parameter, - 'latest_value': None, - 'registered': False + "parameter": self.parameter, + "latest_value": None, + "registered": False } # Add to setpoint list @@ -1462,19 +1462,19 @@ def exit_sweep(self): def execute( self, - *args: Iterable['BaseSweep'], + *args: Iterable["BaseSweep"], name: str = None, measure_params: Union[Iterable, _BaseParameter] = None, repetitions: int = 1, - sweep: Union[Iterable, 'BaseSweep'] = None + sweep: Union[Iterable, "BaseSweep"] = None ) -> DataSetProtocol: # Get "measure_params" from station if not provided if measure_params is None: station = Station.default - if station is None or not getattr(station, 'measure_params', None): + if station is None or not getattr(station, "measure_params", None): raise RuntimeError( - 'Cannot determine parameters to measure. ' - 'Either provide measure_params, or set station.measure_params' + "Cannot determine parameters to measure. " + "Either provide measure_params, or set station.measure_params" ) measure_params = station.measure_params @@ -1485,7 +1485,7 @@ def execute( # Create list of sweeps sweeps = list(args) if not all(isinstance(sweep, BaseSweep) for sweep in sweeps): - raise ValueError('Args passed to Sweep.execute must be Sweeps') + raise ValueError("Args passed to Sweep.execute must be Sweeps") if isinstance(sweep, BaseSweep): sweeps.append(sweep) elif isinstance(sweep, (list, tuple)): @@ -1493,7 +1493,7 @@ def execute( # Add repetition as a sweep if > 1 if repetitions > 1: - repetition_sweep = BaseSweep(range(repetitions), name='repetition') + repetition_sweep = BaseSweep(range(repetitions), name="repetition") sweeps = [repetition_sweep] + sweeps # Add self as innermost sweep @@ -1504,7 +1504,7 @@ def execute( dimensionality = 1 + len(sweeps) sweep_names = [str(sweep.name) for sweep in sweeps] + [str(self.name)] - name = f'{dimensionality}D_sweep_' + '_'.join(sweep_names) + name = f"{dimensionality}D_sweep_" + "_".join(sweep_names) with MeasurementLoop(name) as msmt: measure_sweeps( @@ -1540,10 +1540,10 @@ def post_actions(self) -> ActionsT: class Sweep(BaseSweep): - sequence_keywords = ['start', 'stop', 'around', - 'num', 'step', 'parameter', 'sequence'] - base_keywords = ['delay', 'initial_delay', 'name', - 'label', 'unit', 'revert', 'parameter'] + sequence_keywords = ["start", "stop", "around", + "num", "step", "parameter", "sequence"] + base_keywords = ["delay", "initial_delay", "name", + "label", "unit", "revert", "parameter"] def __init__( self, @@ -1586,8 +1586,8 @@ def _transform_args_to_kwargs(self, *args, **kwargs): Allowed args are: 1 arg: - - Sweep([1,2,3], name='name') - : sweep over sequence [1,2,3] with sweep array name 'name' + - Sweep([1,2,3], name="name") + : sweep over sequence [1,2,3] with sweep array name "name" Note that kwarg "name" must be provided - Sweep(parameter, stop=stop_val) : sweep "parameter" from current value to "stop_val" @@ -1598,8 +1598,8 @@ def _transform_args_to_kwargs(self, *args, **kwargs): : sweep "parameter" over sequence [1,2,3] - Sweep(parameter, stop_val) : sweep "parameter" from current value to "stop_val" - - Sweep([1,2,3], 'name') - : sweep over sequence [1,2,3] with sweep array name 'name' + - Sweep([1,2,3], "name") + : sweep over sequence [1,2,3] with sweep array name "name" 3 args: - Sweep(parameter, start_val, stop_val) : sweep "parameter" from "start_val" to "stop_val" @@ -1609,62 +1609,62 @@ def _transform_args_to_kwargs(self, *args, **kwargs): - Sweep(parameter, start_val, stop_val, num) : Sweep "parameter" from "start_val" to "stop_val" with "num" number of points """ - if len(args) == 1: # Sweep([1,2,3], name='name') + if len(args) == 1: # Sweep([1,2,3], name="name") if isinstance(args[0], Iterable): assert kwargs.get( - 'name') is not None, "Must provide name if sweeping iterable" - kwargs['sequence'], = args + "name") is not None, "Must provide name if sweeping iterable" + kwargs["sequence"], = args elif isinstance(args[0], _BaseParameter): - assert kwargs.get('stop') is not None or kwargs.get('around') is not None, \ + assert kwargs.get("stop") is not None or kwargs.get("around") is not None, \ "Must provide stop value for parameter" - kwargs['parameter'], = args + kwargs["parameter"], = args else: raise SyntaxError( - 'Sweep with 1 arg must have iterable or parameter as arg') + "Sweep with 1 arg must have iterable or parameter as arg") elif len(args) == 2: if isinstance(args[0], _BaseParameter): # Sweep(parameter, [1,2,3]) if isinstance(args[1], Iterable): - kwargs['parameter'], kwargs['sequence'] = args + kwargs["parameter"], kwargs["sequence"] = args elif isinstance(args[1], (int, float)): - kwargs['parameter'], kwargs['stop'] = args + kwargs["parameter"], kwargs["stop"] = args else: raise SyntaxError( - 'Sweep with Parameter arg and second arg should h') - elif isinstance(args[0], Iterable): # Sweep([1,2,3], 'name') + "Sweep with Parameter arg and second arg should h") + elif isinstance(args[0], Iterable): # Sweep([1,2,3], "name") assert isinstance(args[1], str) - assert kwargs.get('name') is None - kwargs['sequence'], kwargs['name'] = args + assert kwargs.get("name") is None + kwargs["sequence"], kwargs["name"] = args else: raise SyntaxError( - 'Unknown sweep syntax. Either use "Sweep(parameter, sequence)" or ' - 'Sweep(sequence, name)"' + "Unknown sweep syntax. Either use 'Sweep(parameter, sequence)' or " + "'Sweep(sequence, name)'" ) elif len(args) == 3: # Sweep(parameter, 0, 1) assert isinstance(args[0], _BaseParameter) assert isinstance(args[1], (float, int)) assert isinstance(args[2], (float, int)) - assert kwargs.get('start') is None - assert kwargs.get('stop') is None - kwargs['parameter'], kwargs['start'], kwargs['stop'] = args + assert kwargs.get("start") is None + assert kwargs.get("stop") is None + kwargs["parameter"], kwargs["start"], kwargs["stop"] = args elif len(args) == 4: # Sweep(parameter, 0, 1, 151) assert isinstance(args[0], _BaseParameter) assert isinstance(args[1], (float, int)) assert isinstance(args[2], (float, int)) assert isinstance(args[3], (float, int)) - assert kwargs.get('start') is None - assert kwargs.get('stop') is None - assert kwargs.get('num') is None - kwargs['parameter'], kwargs['start'], kwargs['stop'], kwargs['num'] = args + assert kwargs.get("start") is None + assert kwargs.get("stop") is None + assert kwargs.get("num") is None + kwargs["parameter"], kwargs["start"], kwargs["stop"], kwargs["num"] = args # Use parameter name, label, and unit if not explicitly provided - if kwargs.get('parameter') is not None: - kwargs.setdefault('name', kwargs['parameter'].name) - kwargs.setdefault('label', kwargs['parameter'].label) - kwargs.setdefault('unit', kwargs['parameter'].unit) + if kwargs.get("parameter") is not None: + kwargs.setdefault("name", kwargs["parameter"].name) + kwargs.setdefault("label", kwargs["parameter"].label) + kwargs.setdefault("unit", kwargs["parameter"].unit) # Update kwargs with sweep_defaults from parameter - if hasattr(kwargs['parameter'], 'sweep_defaults'): - for key, val in kwargs['parameter'].sweep_defaults.items(): + if hasattr(kwargs["parameter"], "sweep_defaults"): + for key, val in kwargs["parameter"].sweep_defaults.items(): if kwargs.get(key) is None: kwargs[key] = val @@ -1693,16 +1693,16 @@ def _generate_sequence( if around is not None: if start is not None or stop is not None: raise SyntaxError( - 'Cannot pass kwarg "around" and also "start" or "stop') + "Cannot pass kwarg 'around' and also 'start' or 'stop'") elif parameter is None: raise SyntaxError( - 'Cannot use kwarg "around" without a parameter') + "Cannot use kwarg 'around' without a parameter") # Convert "around" to "start" and "stop" using parameter current value center_value = parameter() if center_value is None: raise ValueError( - 'Parameter must have initial value if "around" keyword is used') + "Parameter must have initial value if 'around' keyword is used") start = center_value - around stop = center_value + around elif stop is not None: @@ -1710,13 +1710,13 @@ def _generate_sequence( if start is None: if parameter is None: raise SyntaxError( - 'Cannot use "stop" without "start" or a "parameter"') + "Cannot use 'stop' without 'start' or a 'parameter'") start = parameter() if start is None: raise ValueError( - 'Parameter must have initial value if start is not explicitly provided') + "Parameter must have initial value if start is not explicitly provided") else: - raise SyntaxError('Must provide either "around" or "stop"') + raise SyntaxError("Must provide either 'around' or 'stop'") if num is not None: sequence = np.linspace(start, stop, num) @@ -1731,7 +1731,7 @@ def _generate_sequence( sequence = np.append(sequence, [stop]) else: raise SyntaxError( - 'Cannot determine measurement points. Either provide "sequence, "step" or "num"') + "Cannot determine measurement points. Either provide 'sequence', 'step' or 'num'") return sequence From 74ea45ad27394ddf35a5232ce1c6d97450f61b03 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 13 Sep 2022 16:23:30 +0200 Subject: [PATCH 057/122] load Measurement before MeasurementLoop --- qcodes/dataset/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/__init__.py b/qcodes/dataset/__init__.py index aab108cc33e..aef53e9ee52 100644 --- a/qcodes/dataset/__init__.py +++ b/qcodes/dataset/__init__.py @@ -29,8 +29,8 @@ ) from .experiment_settings import get_default_experiment_id, reset_default_experiment_id from .legacy_import import import_dat_file -from .measurement_loop import MeasurementLoop, Sweep from .measurements import Measurement +from .measurement_loop import MeasurementLoop, Sweep from .plotting import plot_by_id, plot_dataset from .sqlite.connection import ConnectionPlus from .sqlite.database import ( From f50595d9df4364b5ad552ece087b406d5e4ab2d0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Sep 2022 14:23:57 +0000 Subject: [PATCH 058/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/__init__.py b/qcodes/dataset/__init__.py index aef53e9ee52..aab108cc33e 100644 --- a/qcodes/dataset/__init__.py +++ b/qcodes/dataset/__init__.py @@ -29,8 +29,8 @@ ) from .experiment_settings import get_default_experiment_id, reset_default_experiment_id from .legacy_import import import_dat_file -from .measurements import Measurement from .measurement_loop import MeasurementLoop, Sweep +from .measurements import Measurement from .plotting import plot_by_id, plot_dataset from .sqlite.connection import ConnectionPlus from .sqlite.database import ( From c15cd8ae87e2244386867a3557416cb7353e1f82 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 13 Sep 2022 16:45:17 +0200 Subject: [PATCH 059/122] Fixing circular imports --- qcodes/dataset/measurement_loop.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 815424d6ae1..98a4d9b2631 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -9,7 +9,9 @@ import numpy as np from qcodes import config as qcodes_config -from qcodes.dataset import AbstractSweep, DataSetProtocol, Measurement +from qcodes.dataset.dond.sweeps import AbstractSweep +from qcodes.dataset.measurements import Measurement +from qcodes.dataset.data_set_protocol import DataSetProtocol from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement from qcodes.dataset.descriptions.rundescriber import RunDescriber from qcodes.dataset.descriptions.versioning import serialization as serial From f984c260f740559e4f08ee03fd889b85e697e082 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Sep 2022 15:06:24 +0000 Subject: [PATCH 060/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 98a4d9b2631..3367f6c95a1 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -9,14 +9,13 @@ import numpy as np from qcodes import config as qcodes_config -from qcodes.dataset.dond.sweeps import AbstractSweep -from qcodes.dataset.measurements import Measurement from qcodes.dataset.data_set_protocol import DataSetProtocol from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement from qcodes.dataset.descriptions.rundescriber import RunDescriber from qcodes.dataset.descriptions.versioning import serialization as serial from qcodes.dataset.descriptions.versioning.converters import new_to_old -from qcodes.dataset.measurements import DataSaver, Runner +from qcodes.dataset.dond.sweeps import AbstractSweep +from qcodes.dataset.measurements import DataSaver, Measurement, Runner from qcodes.dataset.sqlite.queries import add_parameter, update_run_description from qcodes.instrument import ( DelegateParameter, From 30859f80ccd2216509fb95c0763d89e4908b76f4 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 13 Sep 2022 17:17:02 +0200 Subject: [PATCH 061/122] wrote all the documentation for measurement_loop.py --- qcodes/dataset/measurement_loop.py | 257 +++++++++++++++++++++++------ 1 file changed, 203 insertions(+), 54 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 98a4d9b2631..4499fc1043a 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -40,8 +40,11 @@ np.floating, np.bool_, type(None)) -class DatasetHandler: - """Handler for a single DataSet (with Measurement and Runner)""" +class _DatasetHandler: + """Handler for a single DataSet (with Measurement and Runner) + + Used by the `MeasurementLoop` as an interface to the `Measurement` and `DataSet` + """ def __init__(self, measurement_loop: "MeasurementLoop", name="results"): self.measurement_loop = measurement_loop @@ -72,6 +75,7 @@ def __init__(self, measurement_loop: "MeasurementLoop", name="results"): self.initialize() def initialize(self): + """Creates a `Measurement`, runs it and initializes a dataset""" # Once initialized, no new parameters can be added assert not self.initialized, "Cannot initialize twice" @@ -88,10 +92,24 @@ def initialize(self): self.initialized = True def finalize(self): + """Finishes a measurement by flushing all data to the database""" self.datasaver.flush_data_to_database() def _ensure_unique_parameter(self, parameter_info: dict, setpoint: bool, max_idx: int = 99): - """Ensure parameters have unique names""" + """Ensure setpoint / measurement parameters have unique names + + If a previously registered parameter already shares the same name, it adds a + suffix '{name}_{idx}' where idx starts at zero + + Args: + parameter_info: dict for a setpoint/measurement parameter + See `DatasetHandler.create_measurement_info` for more information + setpoints: Whether parameter is a setpoint + max_idx: maximum allowed incremental index when parameters share same name + + Raises: + OverflowError if more than ``max_idx`` parameters share the same name + """ if setpoint: parameter_list = self.setpoint_list else: @@ -131,6 +149,19 @@ def create_measurement_info( label: Optional[str] = None, unit: Optional[str] = None ) -> Dict[str, Any]: + """Creates information dict for a parameter that is to be measured + + Args: + action_indices: Indices in measurement loop corresponding to the + parameter being measured. + parameter: Parameter to be measured. + name: Name used for the measured parameter. + Will use parameter.name if not provided. + label: Label used for the measured parameter. + Will use parameter.label if not provided. + unit: Unit used for the measured parameter. + Will use parameter.unit if not provided. + """ if parameter is None: assert name is not None parameter = Parameter(name=name, label=label, unit=unit) @@ -170,6 +201,7 @@ def register_new_measurement( label: Optional[str] = None, unit: Optional[str] = None ): + """Register a new measurement parameter""" measurement_info = self.create_measurement_info( action_indices=action_indices, parameter=parameter, @@ -238,6 +270,11 @@ def add_measurement_result( measurement_info["latest_value"] = result def _update_interdependencies(self): + """Updates dataset after instantiation to include new setpoint/measurement parameter + + The `DataSet` was not made to register parameters after instantiation, so this + method is non-intuitive. + """ dataset = self.datasaver.dataset # Get previous paramspecs @@ -313,26 +350,17 @@ def _update_interdependencies(self): class MeasurementLoop: - """Class to perform measurements + """Class to perform measurements in a fixed sequential order. + + This measurement method complements the other two ways of doing measurements + by being more versatile than `do1d`, `do2d`, `dond`, and more implicit that `Measurement`. + + See the tutorial ``MeasurementLoop`` for a tutorial. Args: name: Measurement name, also used as the dataset name - force_cell_thread: Enforce that the measurement has been started from a - separate thread if it has been directly executed from an IPython - cell/prompt. This is because a measurement is usually run from a - separate thread using the magic command `%%new_job`. - An error is raised if this has not been satisfied. - Note that if the measurement is started within a function, no error - is raised. notify: Notify when measurement is complete. The function `Measurement.notify_function` must be set - - - Notes: - When the Measurement is started in a separate thread (using %%new_job), - the Measurement is registered in the user namespace as "msmt", and the - dataset as "data" - """ # Context manager @@ -356,7 +384,7 @@ class MeasurementLoop: notify_function = None def __init__( - self, name: Optional[str], force_cell_thread: bool = True, notify: bool = False + self, name: Optional[str], notify: bool = False ): self.name: str = name @@ -387,9 +415,6 @@ def __init__( # Whether to notify upon measurement completion self.notify: bool = notify - # Whether to force measurement to start in new thread - self.force_cell_thread: bool = force_cell_thread and using_ipython() - # Each measurement can have its own final actions, to be executed # regardless of whether the measurement finished successfully or not # Note that there are also Measurement.final_actions, which are always @@ -451,7 +476,7 @@ def measurement_list(self) -> Optional[Dict[Tuple[int], Any]]: return None def __enter__(self): - """Operation when entering a loop""" + """Operation when entering a loop, including dataset instantiation""" self.is_context_manager = True # Encapsulate everything in a try/except to ensure that the context @@ -463,7 +488,7 @@ def __enter__(self): MeasurementLoop.measurement_thread = threading.current_thread() # Initialize dataset handler - self.data_handler = DatasetHandler( + self.data_handler = _DatasetHandler( measurement_loop=self, name=self.name ) @@ -484,7 +509,6 @@ def __enter__(self): self.set_arrays = {} # self.log(f"Measurement started {self.dataset.location}") - # print(f"Measurement started {self.dataset.location}") else: if threading.current_thread() is not MeasurementLoop.measurement_thread: @@ -511,27 +535,6 @@ def __enter__(self): self.data_arrays = msmt.data_arrays self.set_arrays = msmt.set_arrays self.timings = msmt.timings - - # Perform measurement thread check, and set user namespace variables - if self.force_cell_thread and MeasurementLoop.running_measurement is self: - # Raise an error if force_cell_thread is True and the code is run - # directly from an IPython cell/prompt but not from a separate thread - is_main_thread = threading.current_thread() == threading.main_thread() - if is_main_thread and directly_executed_from_cell(): - raise RuntimeError( - "Measurement must be created in dedicated thread. " - "Otherwise specify force_thread=False" - ) - - # Register the Measurement and data as variables in the user namespace - # Usually as variable names are "msmt" and "data" respectively - from IPython import get_ipython - - shell = get_ipython() - shell.user_ns[self._default_measurement_name] = self - # shell.user_ns[self._default_dataset_name] = self.dataset - - return self except: # An error has occured, ensure running_measurement is cleared if MeasurementLoop.running_measurement is self: @@ -633,8 +636,16 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb): def _verify_action(self, action: Callable, name: str, add_if_new: bool = True): """Verify an action corresponds to the current action indices. - This is only relevant if an action has previously been performed at - these action indices + An action is usually (currently always) a measurement. + + Args: + action: Action that is supposed to be performed at these action_indices + add_if_new: Register action if the action_indices have not yet been registered + + Raises: + RuntimeError if a different action is performed than is usually + performed at the current action_indices. An example is when + a different parameter is measuremed. """ if self.action_indices not in self.actions: if add_if_new: @@ -663,7 +674,6 @@ def _apply_actions(self, actions: list, label="", clear=False): actions.clear() # Measurement-related functions - # TODO these methods should always end up with a parameter def _measure_parameter( self, parameter: _BaseParameter, @@ -676,6 +686,16 @@ def _measure_parameter( Called from `measure`. MultiParameter is called separately. + + Args: + parameter: Parameter to be measured + name: Name used to measure parameter, overriding ``parameter.name`` + label: Label used to measure parameter, overriding ``parameter.label`` + unit: Unit used to measure parameter, overriding ``parameter.unit`` + **kwargs: optional kwargs passed to parameter, i.e. ``parameter(**kwargs)`` + + Returns: + Current value of parameter """ name = name or parameter.name @@ -703,6 +723,14 @@ def _measure_multi_parameter( Called from `measure` + Args: + parameter: Parameter to be measured + name: Name used to measure parameter, overriding ``parameter.name`` + **kwargs: optional kwargs passed to parameter, i.e. ``parameter(**kwargs)`` + + Returns: + Current value of parameter + Notes: - Does not store setpoints yet """ @@ -736,7 +764,11 @@ def _measure_callable(self, callable: Callable, name: str = None, **kwargs) -> D The function should return a dict, from which each item is measured. If the function already contains creates a Measurement, the return - values aren"t stored. + values aren't stored. + + Args: + name: Dataset name used for function. Extracts name from function if not provided + **kwargs: optional kwargs passed to callable, i.e. ``callable(**kwargs)`` """ # Determine name if name is None: @@ -781,6 +813,11 @@ def _measure_dict(self, value: dict, name: str) -> Dict[str, Any]: """Store dictionary results Each key is an array name, and the value is the value to store + + Args: + value: dictionary with (str, value) entries. + Each element is a separate dataset array + name: Dataset name used for dictionary """ if not isinstance(value, dict): raise SyntaxError(f"{name} must be a dict, not {value}") @@ -809,6 +846,15 @@ def _measure_value( If this value comes from another parameter acquisition, e.g. from a MultiParameter, the parameter can be passed to use the right set arrays. + + Args: + value: Value to be stored + name: Name used for storage + parameter: optional parameter that is passed on to + `MeasurementLoop.measure` as a kwarg, in which case it's used + for name, label, etc. + label: Optional label for dat array + unit: Optional unit for data array """ if name is None: raise RuntimeError("Must provide a name when measuring a value") @@ -1087,14 +1133,24 @@ def mask(self, obj: Union[object, dict], val: Any = None, **kwargs) -> Any: def unmask( self, - obj: Union[object, dict], + obj: Union[_BaseParameter, object, dict], attr: Optional[str] = None, key: Optional[str] = None, - type=None, + type: Optional[str] = None, value: Optional[Any] = None, raise_exception: bool = True, **kwargs # Add kwargs because original_value may be None ): + """ Unmasks a previously masked object, i.e. revert value back to original + + Args: + obj: Parameter/object/dictionary for which to revert attribute/key + attr: object attribute to revert + key: dictionary key to revert + type: can be 'key', 'attr', 'parameter' if not explicitly provided by kwarg + value: Optional masked value, only used for logging + raise_exception: Whether to raise exception if unmasking fails + """ if "original_value" not in kwargs: # No masked property passed. We collect all the masked properties # that satisfy these requirements and unmask each of them. @@ -1199,7 +1255,7 @@ def skip(self, N: int = 1) -> Tuple[int]: def step_out(self, reduce_dimension: bool = True): """Step out of a Sweep - This function usually doesn"t need to be called. + This function usually doesn't need to be called. """ if MeasurementLoop.running_measurement is not self: MeasurementLoop.running_measurement.step_out( @@ -1232,6 +1288,7 @@ def running_measurement() -> MeasurementLoop: class _IterateDondSweep: + """Class used to encapsulate `AbstractSweep` into `Sweep` as a `Sweep.sequence`""" def __init__(self, sweep: AbstractSweep): self.sweep: AbstractSweep = sweep self.iterator: Iterable = None @@ -1265,10 +1322,15 @@ class BaseSweep(AbstractSweep): Can be an iterable, or a parameter Sweep. If the sequence name: Name of sweep. Not needed if a Parameter is passed + label: Label of sweep. Not needed if a Parameter is passed unit: unit of sweep. Not needed if a Parameter is passed + parameter: Optional parameter that is being swept over. + If provided, the parameter value will be updated every + time the sweep is looped over revert: Stores the state of a parameter before sweeping it, then reverts the original value upon exiting the loop. delay: Wait time after setting value (default zero). + initial_delay: Delay directly after the first element. Examples: ``` @@ -1425,6 +1487,7 @@ def __next__(self) -> Any: return sweep_value def initialize(self) -> Dict[str, Any]: + """Initializes a `Sweep`, attaching it to the current `MeasurementLoop`""" msmt = running_measurement() if msmt.action_indices in msmt.setpoint_list: return msmt.setpoint_list[msmt.action_indices] @@ -1458,18 +1521,34 @@ def initialize(self) -> Dict[str, Any]: return setpoint_info def exit_sweep(self): + """Exits sweep, stepping out of the current `Measurement.action_indices`""": msmt = running_measurement() msmt.step_out(reduce_dimension=True) raise StopIteration def execute( self, - *args: Iterable["BaseSweep"], + *args: Optional[Iterable["BaseSweep"]], name: str = None, measure_params: Union[Iterable, _BaseParameter] = None, repetitions: int = 1, sweep: Union[Iterable, "BaseSweep"] = None ) -> DataSetProtocol: + """Performs a measurement using this sweep + + Args: + *args: Optional additional sweeps used for N-dimensional measurements + The first arg is the outermost sweep dimension, and the sweep on which + `Sweep.execute` was called is the innermost dimension. + name: Dataset name, defaults to a concatenation of sweep parameter names + measure_params: Parameters to measure. + If not provided, it will check the attribute ``Station.measure_params`` + for parameters. Raises an error if undefined. + repetitions: Number of times to repeat measurement, defaults to 1. + This will be the outermost loop if set to a value above 1. + sweep: Identical to passing *args. + Note that ``sweep`` can be either a single Sweep, or a Sweep list. + """ # Get "measure_params" from station if not provided if measure_params is None: station = Station.default @@ -1542,6 +1621,57 @@ def post_actions(self) -> ActionsT: class Sweep(BaseSweep): + """Default class to create a sweep in `do1d`, `do2d`, `dond` and `MeasurementLoop` + + A Sweep can be created through its kwargs (listed below). For the most frequent + use-cases, a Sweep can also be created by passing args in a variety of ways: + + 1 arg: + - Sweep([1,2,3], name="name") + : sweep over sequence [1,2,3] with sweep array name "name" + Note that kwarg "name" must be provided + - Sweep(parameter, stop=stop_val) + : sweep "parameter" from current value to "stop_val" + - Sweep(parameter, around=around_val) + : sweep "parameter" around current value with range "around_val" + : Note that this will set ``revert`` to True if not explicitly False + 2 args: + - Sweep(parameter, [1,2,3]) + : sweep "parameter" over sequence [1,2,3] + - Sweep(parameter, stop_val) + : sweep "parameter" from current value to "stop_val" + - Sweep([1,2,3], "name") + : sweep over sequence [1,2,3] with sweep array name "name" + 3 args: + - Sweep(parameter, start_val, stop_val) + : sweep "parameter" from "start_val" to "stop_val" + If "num" or "step" is not given as kwarg, it will check if "num" or "step" + if set in dict "parameter.sweep_defaults" and use that, or raise an error otherwise. + 4 args: + - Sweep(parameter, start_val, stop_val, num) + : Sweep "parameter" from "start_val" to "stop_val" with "num" number of points + + Args: + start: start value of sweep sequence + Cannot be used together with ``around`` + stop: stop value of sweep sequence + Cannot be used together with ``around`` + around: sweep around the current parameter value. + ``start`` and ``stop`` are then defined from ``around`` and the current vlaue + i.e. start=X-dx, stop=X+dx when current_value=X and around=dx. + Passing the kwarg "around" also sets revert=True unless explicitly set to False. + num: Number of points between start and stop. + Cannot be used together with ``step`` + step: Increment from start to stop. + Cannot be used together with ``num`` + delay: Time delay after incrementing to the next value + initial_delay: Time delay after having incremented to its first value + name: Sweep name, overrides parameter.name + label: Sweep label, overrides parameter.label + unit: Sweep unit, overrides parameter.unit + revert: Revert parameter back to original value after the sweep ends. + This is False by default, unless the kwarg ``around`` is passed + """ sequence_keywords = ["start", "stop", "around", "num", "step", "parameter", "sequence"] base_keywords = ["delay", "initial_delay", "name", @@ -1595,6 +1725,7 @@ def _transform_args_to_kwargs(self, *args, **kwargs): : sweep "parameter" from current value to "stop_val" - Sweep(parameter, around=around_val) : sweep "parameter" around current value with range "around_val" + : Note that this will set ``revert`` to True if not explicitly False 2 args: - Sweep(parameter, [1,2,3]) : sweep "parameter" over sequence [1,2,3] @@ -1670,6 +1801,11 @@ def _transform_args_to_kwargs(self, *args, **kwargs): if kwargs.get(key) is None: kwargs[key] = val + # Revert parameter to original value if kwarg "around" is passed + # and "revert" is not explicitly False + if kwargs["around"] is not None and kwargs["revert"] is None: + kwargs["revert"] = True + sequence_kwargs = {key: kwargs.get(key) for key in self.sequence_keywords} base_kwargs = {key: kwargs.get(key) for key in self.base_keywords} @@ -1739,6 +1875,16 @@ def _generate_sequence( class RepetitionSweep(BaseSweep): + """Basic sweep to repeat something multiple times + Its functionality is comparable to range(N) + + Args: + repetitions: Number of times to loop over + start: Starting index + name: Sweep name, defaults to "repetition" + label: Sweep label, defaults to "Repetition" + unit: Optional sweep unit + """ def __init__( self, repetitions: int, @@ -1761,6 +1907,9 @@ def measure_sweeps( ): """Recursively iterate over Sweep objects, measuring measure_params in innermost loop + This method is used to perform arbitrary-dimension by passing a list of sweeps, + it can be compared to `dond` + Args: sweeps: list of BaseSweep objects to sweep over measure_params: list of parameters to measure in innermost loop From 8345ad9f261562e7adb24ce6a09eb1db492501a4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Sep 2022 15:20:15 +0000 Subject: [PATCH 062/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 93b09837982..1234364f280 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -41,7 +41,7 @@ class _DatasetHandler: """Handler for a single DataSet (with Measurement and Runner) - + Used by the `MeasurementLoop` as an interface to the `Measurement` and `DataSet` """ @@ -96,7 +96,7 @@ def finalize(self): def _ensure_unique_parameter(self, parameter_info: dict, setpoint: bool, max_idx: int = 99): """Ensure setpoint / measurement parameters have unique names - + If a previously registered parameter already shares the same name, it adds a suffix '{name}_{idx}' where idx starts at zero @@ -270,7 +270,7 @@ def add_measurement_result( def _update_interdependencies(self): """Updates dataset after instantiation to include new setpoint/measurement parameter - + The `DataSet` was not made to register parameters after instantiation, so this method is non-intuitive. """ @@ -692,7 +692,7 @@ def _measure_parameter( label: Label used to measure parameter, overriding ``parameter.label`` unit: Unit used to measure parameter, overriding ``parameter.unit`` **kwargs: optional kwargs passed to parameter, i.e. ``parameter(**kwargs)`` - + Returns: Current value of parameter """ @@ -726,7 +726,7 @@ def _measure_multi_parameter( parameter: Parameter to be measured name: Name used to measure parameter, overriding ``parameter.name`` **kwargs: optional kwargs passed to parameter, i.e. ``parameter(**kwargs)`` - + Returns: Current value of parameter @@ -814,7 +814,7 @@ def _measure_dict(self, value: dict, name: str) -> Dict[str, Any]: Each key is an array name, and the value is the value to store Args: - value: dictionary with (str, value) entries. + value: dictionary with (str, value) entries. Each element is a separate dataset array name: Dataset name used for dictionary """ @@ -849,7 +849,7 @@ def _measure_value( Args: value: Value to be stored name: Name used for storage - parameter: optional parameter that is passed on to + parameter: optional parameter that is passed on to `MeasurementLoop.measure` as a kwarg, in which case it's used for name, label, etc. label: Optional label for dat array @@ -1141,7 +1141,7 @@ def unmask( **kwargs # Add kwargs because original_value may be None ): """ Unmasks a previously masked object, i.e. revert value back to original - + Args: obj: Parameter/object/dictionary for which to revert attribute/key attr: object attribute to revert @@ -1534,7 +1534,7 @@ def execute( sweep: Union[Iterable, "BaseSweep"] = None ) -> DataSetProtocol: """Performs a measurement using this sweep - + Args: *args: Optional additional sweeps used for N-dimensional measurements The first arg is the outermost sweep dimension, and the sweep on which From 1fa5df83f4f76ac4adc39114517ea9eb41bb3cb0 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 14 Sep 2022 08:39:13 +0200 Subject: [PATCH 063/122] fix typo --- qcodes/dataset/measurement_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 1234364f280..15fe3ed69cd 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1520,7 +1520,7 @@ def initialize(self) -> Dict[str, Any]: return setpoint_info def exit_sweep(self): - """Exits sweep, stepping out of the current `Measurement.action_indices`""": + """Exits sweep, stepping out of the current `Measurement.action_indices`""" msmt = running_measurement() msmt.step_out(reduce_dimension=True) raise StopIteration From c40c771a7d7aba71db9df9b9bcf5c2ebbc1ac2f4 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 14 Sep 2022 09:04:49 +0200 Subject: [PATCH 064/122] applied pep styling --- qcodes/dataset/__init__.py | 2 +- qcodes/dataset/data_set.py | 2 +- qcodes/dataset/data_set_in_memory.py | 2 +- qcodes/dataset/measurement_loop.py | 233 ++++++++++-------- qcodes/dataset/measurements.py | 8 +- .../test_measurement_loop_basics.py | 15 +- .../test_measurement_loop_sweep.py | 58 ++--- qcodes/utils/helpers.py | 28 ++- 8 files changed, 188 insertions(+), 160 deletions(-) diff --git a/qcodes/dataset/__init__.py b/qcodes/dataset/__init__.py index aab108cc33e..09c276cf1a5 100644 --- a/qcodes/dataset/__init__.py +++ b/qcodes/dataset/__init__.py @@ -88,5 +88,5 @@ "plot_by_id", "plot_dataset", "reset_default_experiment_id", - "Sweep" + "Sweep", ] diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index 78e4f4d464c..8ad17a9f4a8 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -313,7 +313,7 @@ def prepare( shapes: Shapes = None, parent_datasets: Sequence[Mapping[Any, Any]] = (), write_in_background: bool = False, - allow_empty_dataset: bool = False + allow_empty_dataset: bool = False, ) -> None: self.add_snapshot(json.dumps({"station": snapshot}, cls=NumpyJSONEncoder)) diff --git a/qcodes/dataset/data_set_in_memory.py b/qcodes/dataset/data_set_in_memory.py index 3f68bc7a14b..f443a7cce35 100644 --- a/qcodes/dataset/data_set_in_memory.py +++ b/qcodes/dataset/data_set_in_memory.py @@ -401,7 +401,7 @@ def prepare( shapes: Shapes = None, parent_datasets: Sequence[Mapping[Any, Any]] = (), write_in_background: bool = False, - allow_empty_dataset: bool = False + allow_empty_dataset: bool = False, ) -> None: if not self.pristine: raise RuntimeError("Cannot prepare a dataset that is not pristine.") diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 15fe3ed69cd..9baf4a98b7f 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -35,8 +35,16 @@ using_ipython, ) -RAW_VALUE_TYPES = (float, int, bool, np.ndarray, np.integer, - np.floating, np.bool_, type(None)) +RAW_VALUE_TYPES = ( + float, + int, + bool, + np.ndarray, + np.integer, + np.floating, + np.bool_, + type(None) +) class _DatasetHandler: @@ -94,7 +102,9 @@ def finalize(self): """Finishes a measurement by flushing all data to the database""" self.datasaver.flush_data_to_database() - def _ensure_unique_parameter(self, parameter_info: dict, setpoint: bool, max_idx: int = 99): + def _ensure_unique_parameter( + self, parameter_info: dict, setpoint: bool, max_idx: int = 99 + ): """Ensure setpoint / measurement parameters have unique names If a previously registered parameter already shares the same name, it adds a @@ -135,8 +145,7 @@ def _ensure_unique_parameter(self, parameter_info: dict, setpoint: bool, max_idx ) # Create a delegate parameter with modified name delegate_parameter = DelegateParameter( - name=parameter_name, - source=parameter_info["parameter"] + name=parameter_name, source=parameter_info["parameter"] ) parameter_info["dataset_parameter"] = delegate_parameter @@ -146,7 +155,7 @@ def create_measurement_info( parameter: _BaseParameter, name: Optional[str] = None, label: Optional[str] = None, - unit: Optional[str] = None + unit: Optional[str] = None, ) -> Dict[str, Any]: """Creates information dict for a parameter that is to be measured @@ -164,18 +173,14 @@ def create_measurement_info( if parameter is None: assert name is not None parameter = Parameter(name=name, label=label, unit=unit) - elif {name, label, unit} != {None, }: + elif {name, label, unit} != { + None, + }: + overwrite_attrs = {"name": name, "label": label, "unit": unit} overwrite_attrs = { - "name": name, - "label": label, - "unit": unit + key: val for key, val in overwrite_attrs.items() if val is not None } - overwrite_attrs = {key: val for key, - val in overwrite_attrs.items() if val is not None} - parameter = DelegateParameter( - source=parameter, - **overwrite_attrs - ) + parameter = DelegateParameter(source=parameter, **overwrite_attrs) setpoints_action_indices = [] for k in range(len(action_indices) + 1): @@ -187,7 +192,7 @@ def create_measurement_info( "setpoints_action_indices": setpoints_action_indices, "shape": self.measurement_loop.loop_shape, "unstored_results": [], - "registered": False + "registered": False, } return measurement_info @@ -198,7 +203,7 @@ def register_new_measurement( parameter: _BaseParameter, name: Optional[str] = None, label: Optional[str] = None, - unit: Optional[str] = None + unit: Optional[str] = None, ): """Register a new measurement parameter""" measurement_info = self.create_measurement_info( @@ -206,7 +211,7 @@ def register_new_measurement( parameter=parameter, name=name, label=label, - unit=unit + unit=unit, ) self.measurement_list[action_indices] = measurement_info @@ -240,7 +245,7 @@ def add_measurement_result( parameter=parameter, name=name, label=label, - unit=unit + unit=unit, ) measurement_info = self.measurement_list[action_indices] @@ -307,13 +312,12 @@ def _update_interdependencies(self): self._ensure_unique_parameter(measurement_info, setpoint=False) self.measurement.register_parameter( measurement_info["dataset_parameter"], - setpoints=measurement_info["setpoint_parameters"] + setpoints=measurement_info["setpoint_parameters"], ) measurement_info["registered"] = True self.measurement.set_shapes( detect_shape_of_measurement( - (measurement_info["dataset_parameter"], - ), measurement_info["shape"] + (measurement_info["dataset_parameter"],), measurement_info["shape"] ) ) @@ -382,9 +386,7 @@ class MeasurementLoop: # The last three are only not None if an error has occured notify_function = None - def __init__( - self, name: Optional[str], notify: bool = False - ): + def __init__(self, name: Optional[str], notify: bool = False): self.name: str = name # Data handler is created during `with Measurement("name")` @@ -488,8 +490,7 @@ def __enter__(self): # Initialize dataset handler self.data_handler = _DatasetHandler( - measurement_loop=self, - name=self.name + measurement_loop=self, name=self.name ) # TODO incorporate metadata @@ -520,9 +521,10 @@ def __enter__(self): # a data_group of the primary measurement msmt = MeasurementLoop.running_measurement msmt.data_groups[msmt.action_indices] = self - data_groups = [ - (key, getattr(val, "name", "None")) for key, val in msmt.data_groups.items() - ] + # data_groups = [ + # (key, getattr(val, "name", "None")) + # for key, val in msmt.data_groups.items() + # ] # TODO add metadata # msmt.dataset.add_metadata({"data_groups": data_groups}) msmt.action_indices += (0,) @@ -555,11 +557,9 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb): MeasurementLoop.running_measurement = None if exc_type is not None: - self.log( - f"Measurement error {exc_type.__name__}({exc_val})", level="error") + self.log(f"Measurement error {exc_type.__name__}({exc_val})", level="error") - self._apply_actions(self.except_actions, - label="except", clear=True) + self._apply_actions(self.except_actions, label="except", clear=True) if msmt is self: self._apply_actions( @@ -573,8 +573,7 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb): if msmt is self: # Also perform global final actions # These are always performed when outermost measurement finishes - self._apply_actions( - MeasurementLoop.final_actions, label="global final") + self._apply_actions(MeasurementLoop.final_actions, label="global final") # Notify that measurement is complete if self.notify and self.notify_function is not None: @@ -679,8 +678,8 @@ def _measure_parameter( name: Optional[str] = None, label: Optional[str] = None, unit: Optional[str] = None, - **kwargs - ) -> Any: + **kwargs, + ) -> Any: """Measure parameter and store results. Called from `measure`. @@ -758,7 +757,9 @@ def _measure_multi_parameter( return results - def _measure_callable(self, callable: Callable, name: str = None, **kwargs) -> Dict[str, Any]: + def _measure_callable( + self, callable: Callable, name: str = None, **kwargs + ) -> Dict[str, Any]: """Measure a callable (function) and store results The function should return a dict, from which each item is measured. @@ -778,8 +779,7 @@ def _measure_callable(self, callable: Callable, name: str = None, **kwargs) -> D elif hasattr(callable, "__name__"): name = callable.__name__ else: - action_indices_str = "_".join(str(idx) - for idx in self.action_indices) + action_indices_str = "_".join(str(idx) for idx in self.action_indices) name = f"data_group_{action_indices_str}" # Ensure measuring callable matches the current action_indices @@ -822,8 +822,7 @@ def _measure_dict(self, value: dict, name: str) -> Dict[str, Any]: raise SyntaxError(f"{name} must be a dict, not {value}") if not isinstance(name, str) or name == "": - raise SyntaxError( - f"Dict result {name} must have a valid name: {value}") + raise SyntaxError(f"Dict result {name} must have a valid name: {value}") # Ensure measuring callable matches the current action_indices self._verify_action(action=None, name=name, add_if_new=True) @@ -840,7 +839,8 @@ def _measure_value( name: str, parameter: Optional[_BaseParameter] = None, label: Optional[str] = None, - unit: Optional[str] = None) -> Union[float, int, bool]: + unit: Optional[str] = None, + ) -> Union[float, int, bool]: """Store a single value (float/int/bool) If this value comes from another parameter acquisition, e.g. from a @@ -949,8 +949,12 @@ def measure( t_now = datetime.now() # Store time referenced to t_start - self.measure((t_now - self._t_start).total_seconds(), - "T_pre", unit="s", timestamp=False) + self.measure( + (t_now - self._t_start).total_seconds(), + "T_pre", + unit="s", + timestamp=False + ) self.skip() # Increment last action index by 1 # TODO Incorporate kwargs name, label, and unit, into each of these @@ -960,15 +964,15 @@ def measure( ) self.skip() # Increment last action index by 1 elif isinstance(measurable, MultiParameter): - result = self._measure_multi_parameter( - measurable, name=name, **kwargs) + result = self._measure_multi_parameter(measurable, name=name, **kwargs) elif callable(measurable): result = self._measure_callable(measurable, name=name, **kwargs) elif isinstance(measurable, dict): result = self._measure_dict(measurable, name=name) elif isinstance(measurable, RAW_VALUE_TYPES): result = self._measure_value( - measurable, name=name, label=label, unit=unit, **kwargs) + measurable, name=name, label=label, unit=unit, **kwargs + ) self.skip() # Increment last action index by 1 else: raise RuntimeError( @@ -980,13 +984,16 @@ def measure( t_now = datetime.now() # Store time referenced to t_start - self.measure((t_now - self._t_start).total_seconds(), - "T_post", unit="s", timestamp=False) + self.measure( + (t_now - self._t_start).total_seconds(), + "T_post", + unit="s", + timestamp=False + ) self.skip() # Increment last action index by 1 self.timings.record( - ["measurement", initial_action_indices, "total"], - perf_counter() - t0 + ["measurement", initial_action_indices, "total"], perf_counter() - t0 ) return result @@ -1138,9 +1145,9 @@ def unmask( type: Optional[str] = None, value: Optional[Any] = None, raise_exception: bool = True, - **kwargs # Add kwargs because original_value may be None + **kwargs, # Add kwargs because original_value may be None ): - """ Unmasks a previously masked object, i.e. revert value back to original + """Unmasks a previously masked object, i.e. revert value back to original Args: obj: Parameter/object/dictionary for which to revert attribute/key @@ -1258,7 +1265,8 @@ def step_out(self, reduce_dimension: bool = True): """ if MeasurementLoop.running_measurement is not self: MeasurementLoop.running_measurement.step_out( - reduce_dimension=reduce_dimension) + reduce_dimension=reduce_dimension + ) else: if reduce_dimension: self.loop_shape = self.loop_shape[:-1] @@ -1272,11 +1280,10 @@ def step_out(self, reduce_dimension: bool = True): def traceback(self): """Print traceback if an error occurred. - Measurement must be ran from separate thread + Measurement must be ran from separate thread """ if self.measurement_thread is None: - raise RuntimeError( - "Measurement was not started in separate thread") + raise RuntimeError("Measurement was not started in separate thread") else: self.measurement_thread.traceback() @@ -1288,6 +1295,7 @@ def running_measurement() -> MeasurementLoop: class _IterateDondSweep: """Class used to encapsulate `AbstractSweep` into `Sweep` as a `Sweep.sequence`""" + def __init__(self, sweep: AbstractSweep): self.sweep: AbstractSweep = sweep self.iterator: Iterable = None @@ -1351,13 +1359,12 @@ def __init__( parameter: Optional[_BaseParameter] = None, revert: bool = False, delay: Optional[float] = None, - initial_delay: Optional[float] = None - ): + initial_delay: Optional[float] = None, + ): if isinstance(sequence, AbstractSweep): sequence = _IterateDondSweep(sequence) elif not isinstance(sequence, Iterable): - raise SyntaxError( - f"Sweep sequence must be iterable, not {type(sequence)}") + raise SyntaxError(f"Sweep sequence must be iterable, not {type(sequence)}") # Properties for the data array self.name: Optional[str] = name @@ -1391,8 +1398,7 @@ def __repr__(self) -> str: components.append(f"{self.name}") # Add number of elements - num_elems = str(len(self.sequence) - ) if self.sequence is not None else "unknown" + num_elems = str(len(self.sequence)) if self.sequence is not None else "unknown" components.append(f"length={num_elems}") # Combine components @@ -1415,11 +1421,9 @@ def __iter__(self) -> Iterable: if self.revert: if isinstance(self.sequence, SweepValues): - msmt.mask(self.sequence.parameter, - self.sequence.parameter.get()) + msmt.mask(self.sequence.parameter, self.sequence.parameter.get()) else: - raise NotImplementedError( - "Unable to revert non-parameter values.") + raise NotImplementedError("Unable to revert non-parameter values.") self.loop_index = 0 self.dimension = len(msmt.loop_shape) @@ -1499,15 +1503,13 @@ def initialize(self) -> Dict[str, Any]: else: # Need to create a parameter self.parameter = Parameter( - name=self.name, - label=self.label, - unit=self.unit + name=self.name, label=self.label, unit=self.unit ) setpoint_info = { "parameter": self.parameter, "latest_value": None, - "registered": False + "registered": False, } # Add to setpoint list @@ -1531,7 +1533,7 @@ def execute( name: str = None, measure_params: Union[Iterable, _BaseParameter] = None, repetitions: int = 1, - sweep: Union[Iterable, "BaseSweep"] = None + sweep: Union[Iterable, "BaseSweep"] = None, ) -> DataSetProtocol: """Performs a measurement using this sweep @@ -1582,13 +1584,11 @@ def execute( # Determine "name" if not provided from sweeps if name is None: dimensionality = 1 + len(sweeps) - sweep_names = [str(sweep.name) - for sweep in sweeps] + [str(self.name)] + sweep_names = [str(sweep.name) for sweep in sweeps] + [str(self.name)] name = f"{dimensionality}D_sweep_" + "_".join(sweep_names) with MeasurementLoop(name) as msmt: - measure_sweeps( - sweeps=sweeps, measure_params=measure_params, msmt=msmt) + measure_sweeps(sweeps=sweeps, measure_params=measure_params, msmt=msmt) return msmt.dataset @@ -1671,10 +1671,24 @@ class Sweep(BaseSweep): revert: Revert parameter back to original value after the sweep ends. This is False by default, unless the kwarg ``around`` is passed """ - sequence_keywords = ["start", "stop", "around", - "num", "step", "parameter", "sequence"] - base_keywords = ["delay", "initial_delay", "name", - "label", "unit", "revert", "parameter"] + sequence_keywords = [ + "start", + "stop", + "around", + "num", + "step", + "parameter", + "sequence", + ] + base_keywords = [ + "delay", + "initial_delay", + "name", + "label", + "unit", + "revert", + "parameter", + ] def __init__( self, @@ -1689,7 +1703,7 @@ def __init__( name: str = None, label: str = None, unit: str = None, - revert: bool = None + revert: bool = None, ): kwargs = dict( start=start, @@ -1705,8 +1719,7 @@ def __init__( revert=revert ) - sequence_kwargs, base_kwargs = self._transform_args_to_kwargs( - *args, **kwargs) + sequence_kwargs, base_kwargs = self._transform_args_to_kwargs(*args, **kwargs) self.sequence: Iterable = self._generate_sequence(**sequence_kwargs) @@ -1743,16 +1756,19 @@ def _transform_args_to_kwargs(self, *args, **kwargs): """ if len(args) == 1: # Sweep([1,2,3], name="name") if isinstance(args[0], Iterable): - assert kwargs.get( - "name") is not None, "Must provide name if sweeping iterable" - kwargs["sequence"], = args + assert ( + kwargs.get("name") is not None + ), "Must provide name if sweeping iterable" + (kwargs["sequence"],) = args elif isinstance(args[0], _BaseParameter): - assert kwargs.get("stop") is not None or kwargs.get("around") is not None, \ - "Must provide stop value for parameter" - kwargs["parameter"], = args + assert ( + kwargs.get("stop") is not None or kwargs.get("around") is not None + ), "Must provide stop value for parameter" + (kwargs["parameter"],) = args else: raise SyntaxError( - "Sweep with 1 arg must have iterable or parameter as arg") + "Sweep with 1 arg must have iterable or parameter as arg" + ) elif len(args) == 2: if isinstance(args[0], _BaseParameter): # Sweep(parameter, [1,2,3]) if isinstance(args[1], Iterable): @@ -1761,7 +1777,9 @@ def _transform_args_to_kwargs(self, *args, **kwargs): kwargs["parameter"], kwargs["stop"] = args else: raise SyntaxError( - "Sweep with Parameter arg and second arg should h") + "Sweep with Parameter arg and second arg should have second arg be either " + "a sequence or a target value" + ) elif isinstance(args[0], Iterable): # Sweep([1,2,3], "name") assert isinstance(args[1], str) assert kwargs.get("name") is None @@ -1805,8 +1823,7 @@ def _transform_args_to_kwargs(self, *args, **kwargs): if kwargs["around"] is not None and kwargs["revert"] is None: kwargs["revert"] = True - sequence_kwargs = {key: kwargs.get(key) - for key in self.sequence_keywords} + sequence_kwargs = {key: kwargs.get(key) for key in self.sequence_keywords} base_kwargs = {key: kwargs.get(key) for key in self.base_keywords} return sequence_kwargs, base_kwargs @@ -1819,7 +1836,7 @@ def _generate_sequence( num: Optional[int] = None, step: Optional[float] = None, parameter: Optional[_BaseParameter] = None, - sequence: Optional[Iterable] = None + sequence: Optional[Iterable] = None, ): """Creates a sequence from passed values""" # Return "sequence" if explicitly provided @@ -1830,28 +1847,29 @@ def _generate_sequence( if around is not None: if start is not None or stop is not None: raise SyntaxError( - "Cannot pass kwarg 'around' and also 'start' or 'stop'") + "Cannot pass kwarg 'around' and also 'start' or 'stop'" + ) elif parameter is None: - raise SyntaxError( - "Cannot use kwarg 'around' without a parameter") + raise SyntaxError("Cannot use kwarg 'around' without a parameter") # Convert "around" to "start" and "stop" using parameter current value center_value = parameter() if center_value is None: raise ValueError( - "Parameter must have initial value if 'around' keyword is used") + "Parameter must have initial value if 'around' keyword is used" + ) start = center_value - around stop = center_value + around elif stop is not None: # Use "parameter" current value if "start" is not provided if start is None: if parameter is None: - raise SyntaxError( - "Cannot use 'stop' without 'start' or a 'parameter'") + raise SyntaxError("Cannot use 'stop' without 'start' or a 'parameter'") start = parameter() if start is None: raise ValueError( - "Parameter must have initial value if start is not explicitly provided") + "Parameter must have initial value if start is not explicitly provided" + ) else: raise SyntaxError("Must provide either 'around' or 'stop'") @@ -1868,7 +1886,8 @@ def _generate_sequence( sequence = np.append(sequence, [stop]) else: raise SyntaxError( - "Cannot determine measurement points. Either provide 'sequence', 'step' or 'num'") + "Cannot determine measurement points. Either provide 'sequence', 'step' or 'num'" + ) return sequence @@ -1884,6 +1903,7 @@ class RepetitionSweep(BaseSweep): label: Sweep label, defaults to "Repetition" unit: Optional sweep unit """ + def __init__( self, repetitions: int, @@ -1902,7 +1922,7 @@ def __init__( def measure_sweeps( sweeps: list[BaseSweep], measure_params: list[_BaseParameter], - msmt: MeasurementLoop = None + msmt: MeasurementLoop = None, ): """Recursively iterate over Sweep objects, measuring measure_params in innermost loop @@ -1913,6 +1933,7 @@ def measure_sweeps( sweeps: list of BaseSweep objects to sweep over measure_params: list of parameters to measure in innermost loop """ + if sweeps: outer_sweep, *inner_sweeps = sweeps diff --git a/qcodes/dataset/measurements.py b/qcodes/dataset/measurements.py index 96b688fc0e4..878c70fbf98 100644 --- a/qcodes/dataset/measurements.py +++ b/qcodes/dataset/measurements.py @@ -511,7 +511,7 @@ def __init__( shapes: Shapes | None = None, in_memory_cache: bool = True, dataset_class: DataSetType = DataSetType.DataSet, - allow_empty_dataset: bool = False + allow_empty_dataset: bool = False, ) -> None: self._dataset_class = dataset_class @@ -611,7 +611,7 @@ def __enter__(self) -> DataSaver: write_in_background=self._write_in_background, shapes=self._shapes, parent_datasets=self._parent_datasets, - allow_empty_dataset=self.allow_empty_dataset + allow_empty_dataset=self.allow_empty_dataset, ) # register all subscribers @@ -1232,7 +1232,7 @@ def run( write_in_background: bool | None = None, in_memory_cache: bool = True, dataset_class: DataSetType = DataSetType.DataSet, - allow_empty_dataset: bool = False + allow_empty_dataset: bool = False, ) -> Runner: """ Returns the context manager for the experimental run @@ -1266,5 +1266,5 @@ def run( shapes=self._shapes, in_memory_cache=in_memory_cache, dataset_class=dataset_class, - allow_empty_dataset=allow_empty_dataset + allow_empty_dataset=allow_empty_dataset, ) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 52a11e4eb4c..d84fed87b0b 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -25,10 +25,7 @@ def test_original_dond(): p1_get = ManualParameter("p1_get", initial_value=1) p2_get = ManualParameter("p2_get", initial_value=1) p1_set = ManualParameter("p1_set", initial_value=1) - dond( - p1_set, 0, 1, 101, - p1_get, p2_get - ) + dond(p1_set, 0, 1, 101, p1_get, p2_get) def test_create_measurement(): @@ -138,7 +135,7 @@ def test_1D_measurement_duplicate_getset(): arrays = data.get_parameter_data() offsets = {"p1_get": 1, "p1_get_1": 0.5} - for suffix in ['', '_1']: + for suffix in ["", "_1"]: get_key = f"p1_get{suffix}" set_key = f"p1_set{suffix}" data_arrays = arrays[get_key] @@ -216,8 +213,8 @@ def nested_measurement(): def test_measurement_no_parameter(): with MeasurementLoop("test") as msmt: - for val in Sweep(np.linspace(0, 1, 11), 'p1_set', label='p1 label', unit='V'): - msmt.measure(val+1, name='p1_get') + for val in Sweep(np.linspace(0, 1, 11), "p1_set", label="p1 label", unit="V"): + msmt.measure(val+1, name="p1_get") data = msmt.dataset assert data.name == "test" @@ -232,7 +229,7 @@ def test_measurement_no_parameter(): # def test_measurement_percentage_complete(): # with MeasurementLoop("test") as msmt: -# for val in Sweep(np.linspace(0, 1, 11), 'p1_set'): +# for val in Sweep(np.linspace(0, 1, 11), "p1_set"): # print(msmt.percentage_complete()) -# msmt.measure(val+1, name='p1_get') +# msmt.measure(val+1, name="p1_get") # print(msmt.percentage_complete()) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index 35edea40a35..3122ad4d796 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -19,12 +19,12 @@ def test_sweep_1_arg_sequence(): - sequence = [1,2,3] - sweep = Sweep(sequence, name='sweep_name') + sequence = [1, 2, 3] + sweep = Sweep(sequence, name="sweep_name") assert sweep.sequence == sequence def test_sweep_1_arg_parameter_stop(): - sweep_parameter = ManualParameter('sweep_parameter') + sweep_parameter = ManualParameter("sweep_parameter") # Should raise an error since it does not have an initial value with pytest.raises(ValueError): @@ -34,24 +34,24 @@ def test_sweep_1_arg_parameter_stop(): sweep = Sweep(sweep_parameter, stop=10, num=21) assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) - sweep_parameter.sweep_defaults = {'num': 21} + sweep_parameter.sweep_defaults = {"num": 21} sweep = Sweep(sweep_parameter, stop=10) assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) def test_sweep_1_arg_parameter_around(): - sweep_parameter = ManualParameter('sweep_parameter', initial_value=0) + sweep_parameter = ManualParameter("sweep_parameter", initial_value=0) sweep = Sweep(sweep_parameter, around=5, num=21) assert np.allclose(sweep.sequence, np.linspace(-5, 5, 21)) - sweep_parameter.sweep_defaults = {'num': 21} + sweep_parameter.sweep_defaults = {"num": 21} sweep = Sweep(sweep_parameter, around=5) assert np.allclose(sweep.sequence, np.linspace(-5, 5, 21)) def test_sweep_2_args_parameter_sequence(): - sweep_parameter = ManualParameter('sweep_parameter', initial_value=0) + sweep_parameter = ManualParameter("sweep_parameter", initial_value=0) sequence = [1, 2, 3] sweep = Sweep(sweep_parameter, sequence) @@ -60,7 +60,7 @@ def test_sweep_2_args_parameter_sequence(): def test_sweep_2_args_parameter_stop(): - sweep_parameter = ManualParameter('sweep_parameter') + sweep_parameter = ManualParameter("sweep_parameter") # No initial value with pytest.raises(ValueError): @@ -75,7 +75,7 @@ def test_sweep_2_args_parameter_stop(): sweep = Sweep(sweep_parameter, 10, num=21) assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) - sweep_parameter.sweep_defaults = {'num': 21} + sweep_parameter.sweep_defaults = {"num": 21} sweep = Sweep(sweep_parameter, 10) assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) @@ -90,7 +90,7 @@ def test_sweep_2_args_sequence_name(): def test_sweep_3_args_parameter_start_stop(): - sweep_parameter = ManualParameter('sweep_parameter') + sweep_parameter = ManualParameter("sweep_parameter") with pytest.raises(SyntaxError): sweep = Sweep(sweep_parameter, 0, 10) @@ -107,7 +107,7 @@ def test_sweep_3_args_parameter_start_stop(): def test_sweep_4_args_parameter_start_stop_num(): - sweep_parameter = ManualParameter('sweep_parameter') + sweep_parameter = ManualParameter("sweep_parameter") sweep = Sweep(sweep_parameter, 0, 10, 21) assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) @@ -127,7 +127,7 @@ def test_sweep_len(): assert len(sweep) == 21 def test_error_on_iterate_sweep(): - sweep = Sweep([1,2,3], 'sweep') + sweep = Sweep([1, 2, 3], "sweep") with pytest.raises(RuntimeError): iter(sweep) @@ -135,35 +135,37 @@ def test_error_on_iterate_sweep(): @pytest.mark.usefixtures("empty_temp_db", "experiment") def test_sweep_in_dond(): - set_parameter = ManualParameter('set_param') - sweep = Sweep(set_parameter, [1,2,3]) - get_parameter = Parameter('get_param', get_cmd=set_parameter) + set_parameter = ManualParameter("set_param") + sweep = Sweep(set_parameter, [1, 2, 3]) + get_parameter = Parameter("get_param", get_cmd=set_parameter) dataset, _, _ = dond(sweep, get_parameter) - assert np.allclose(dataset.get_parameter_data('get_param')['get_param']['get_param'], [1,2,3]) + assert np.allclose( + dataset.get_parameter_data("get_param")["get_param"]["get_param"], [1, 2, 3] + ) @pytest.mark.usefixtures("empty_temp_db", "experiment") def test_sweep_and_linsweep_in_dond(): - set_parameter = ManualParameter('set_param') + set_parameter = ManualParameter("set_param") - sweep = Sweep(set_parameter, [1,2,3]) + sweep = Sweep(set_parameter, [1, 2, 3]) - set_parameter2 = ManualParameter('set_param2') + set_parameter2 = ManualParameter("set_param2") linsweep = LinSweep(set_parameter2, 0, 10, 11) - get_parameter = Parameter('get_param', get_cmd=set_parameter) + get_parameter = Parameter("get_param", get_cmd=set_parameter) dataset, _, _ = dond(sweep, linsweep, get_parameter) - arr = dataset.get_parameter_data('get_param')['get_param']['get_param'] + arr = dataset.get_parameter_data("get_param")["get_param"]["get_param"] - assert np.allclose(arr, np.repeat(np.array([1,2,3])[:,np.newaxis], 11, axis=1)) + assert np.allclose(arr, np.repeat(np.array([1, 2, 3])[:, np.newaxis], 11, axis=1)) def test_sweep_execute_sweep_args(): - set_parameter = ManualParameter('set_param') - sweep = Sweep(set_parameter, [1,2,3]) - set_parameter2 = ManualParameter('set_param2') - other_sweep = Sweep(set_parameter2, [1,2,3]) + set_parameter = ManualParameter("set_param") + sweep = Sweep(set_parameter, [1, 2, 3]) + set_parameter2 = ManualParameter("set_param2") + other_sweep = Sweep(set_parameter2, [1, 2, 3]) get_param = Parameter( "get_param", get_cmd=lambda: set_parameter() + set_parameter2() @@ -171,6 +173,6 @@ def test_sweep_execute_sweep_args(): dataset = sweep.execute(other_sweep, measure_params=get_param) - arr = dataset.get_parameter_data('get_param')['get_param']['get_param'] - assert np.allclose(arr, [[2,3,4], [3,4,5], [4,5,6]]) + arr = dataset.get_parameter_data("get_param")["get_param"]["get_param"] + assert np.allclose(arr, [[2, 3, 4], [3, 4, 5], [4, 5, 6]]) print(dataset) diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index bc592089672..ea17e2b9ca7 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -55,7 +55,7 @@ def warn_units(class_name: str, instance: object) -> None: def using_ipython() -> bool: """Check if code is run from IPython (including jupyter notebook/lab)""" - return hasattr(builtins, '__IPYTHON__') + return hasattr(builtins, "__IPYTHON__") def directly_executed_from_cell(level: int = 1) -> bool: @@ -84,10 +84,10 @@ def directly_executed_from_cell(level: int = 1) -> bool: """ if level < 1: - raise SyntaxError('Level must be 1 or higher') + raise SyntaxError("Level must be 1 or higher") frame = sys._getframe(level) - return '_' in frame.f_locals + return "_" in frame.f_locals def get_last_input_cells(cells=3): @@ -98,14 +98,22 @@ def get_last_input_cells(cells=3): last cell input if successful, else None """ global In - if 'In' in globals() or hasattr(builtins, 'In'): + if "In" in globals() or hasattr(builtins, "In"): return In[-cells:] else: - logging.warning('No input cells found') + logging.warning("No input cells found") def get_exponent(val): - prefactors = [(9, 'G'), (6, 'M'), (3, 'k'), (0, ''), (-3, 'm'), (-6, 'u'), (-9, 'n')] + prefactors = [ + (9, "G"), + (6, "M"), + (3, "k"), + (0, ""), + (-3, "m"), + (-6, "u"), + (-9, "n") + ] for exponent, prefactor in prefactors: if val >= np.power(10., exponent): return exponent, prefactor @@ -113,7 +121,7 @@ def get_exponent(val): return prefactors[-1] -class PerformanceTimer(): +class PerformanceTimer: max_records = 100 def __init__(self): @@ -132,9 +140,9 @@ def clear(self): def _timing_to_str(self, val): mean_val = np.mean(val) exponent, prefactor = get_exponent(mean_val) - factor = np.power(10., exponent) + factor = np.power(10.0, exponent) - return f'{mean_val / factor:.3g}+-{np.abs(np.std(val))/factor:.3g} {prefactor}s' + return f"{mean_val / factor:.3g}+-{np.abs(np.std(val))/factor:.3g} {prefactor}s" def _timings_to_str(self, d: dict): @@ -156,7 +164,7 @@ def record(self, key, val=None): d = self.timings.create_dicts(*parent_keys) timing_list = d.setdefault(subkey, []) else: - raise ValueError('Key must be str or list/tuple') + raise ValueError("Key must be str or list/tuple") if val is not None: timing_list.append(val) From deb0a126167cc47e53fb461067d73eec7350c1cd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 14 Sep 2022 07:05:42 +0000 Subject: [PATCH 065/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 46 +++++++++++++++--------------- qcodes/utils/helpers.py | 12 ++++---- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 9baf4a98b7f..94a3d84e263 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -36,13 +36,13 @@ ) RAW_VALUE_TYPES = ( - float, - int, - bool, - np.ndarray, + float, + int, + bool, + np.ndarray, np.integer, - np.floating, - np.bool_, + np.floating, + np.bool_, type(None) ) @@ -174,7 +174,7 @@ def create_measurement_info( assert name is not None parameter = Parameter(name=name, label=label, unit=unit) elif {name, label, unit} != { - None, + None, }: overwrite_attrs = {"name": name, "label": label, "unit": unit} overwrite_attrs = { @@ -522,7 +522,7 @@ def __enter__(self): msmt = MeasurementLoop.running_measurement msmt.data_groups[msmt.action_indices] = self # data_groups = [ - # (key, getattr(val, "name", "None")) + # (key, getattr(val, "name", "None")) # for key, val in msmt.data_groups.items() # ] # TODO add metadata @@ -951,8 +951,8 @@ def measure( # Store time referenced to t_start self.measure( (t_now - self._t_start).total_seconds(), - "T_pre", - unit="s", + "T_pre", + unit="s", timestamp=False ) self.skip() # Increment last action index by 1 @@ -986,8 +986,8 @@ def measure( # Store time referenced to t_start self.measure( (t_now - self._t_start).total_seconds(), - "T_post", - unit="s", + "T_post", + unit="s", timestamp=False ) self.skip() # Increment last action index by 1 @@ -1672,21 +1672,21 @@ class Sweep(BaseSweep): This is False by default, unless the kwarg ``around`` is passed """ sequence_keywords = [ - "start", - "stop", + "start", + "stop", "around", - "num", - "step", - "parameter", + "num", + "step", + "parameter", "sequence", ] base_keywords = [ - "delay", - "initial_delay", + "delay", + "initial_delay", "name", - "label", - "unit", - "revert", + "label", + "unit", + "revert", "parameter", ] @@ -1933,7 +1933,7 @@ def measure_sweeps( sweeps: list of BaseSweep objects to sweep over measure_params: list of parameters to measure in innermost loop """ - + if sweeps: outer_sweep, *inner_sweeps = sweeps diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index ea17e2b9ca7..9677eab51d8 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -106,12 +106,12 @@ def get_last_input_cells(cells=3): def get_exponent(val): prefactors = [ - (9, "G"), - (6, "M"), - (3, "k"), - (0, ""), - (-3, "m"), - (-6, "u"), + (9, "G"), + (6, "M"), + (3, "k"), + (0, ""), + (-3, "m"), + (-6, "u"), (-9, "n") ] for exponent, prefactor in prefactors: From b9519df09cdf57b070fa2a5d7c73563410c282c9 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 14 Sep 2022 09:06:10 +0200 Subject: [PATCH 066/122] fix pytest error --- qcodes/dataset/measurement_loop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 9baf4a98b7f..c61a90f2b46 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1922,7 +1922,7 @@ def __init__( def measure_sweeps( sweeps: list[BaseSweep], measure_params: list[_BaseParameter], - msmt: MeasurementLoop = None, + msmt: "MeasurementLoop" = None, ): """Recursively iterate over Sweep objects, measuring measure_params in innermost loop @@ -1933,7 +1933,7 @@ def measure_sweeps( sweeps: list of BaseSweep objects to sweep over measure_params: list of parameters to measure in innermost loop """ - + if sweeps: outer_sweep, *inner_sweeps = sweeps From 70e08960e7260dcf8278fed21f0c59a14591219c Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 14 Sep 2022 09:09:57 +0200 Subject: [PATCH 067/122] second round of pep fixes --- qcodes/dataset/measurement_loop.py | 19 ++++++++++--------- .../test_measurement_loop_basics.py | 2 +- .../test_measurement_loop_sweep.py | 2 ++ qcodes/utils/helpers.py | 4 ++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index edcb6948f95..7cabf17a51d 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -43,7 +43,7 @@ np.integer, np.floating, np.bool_, - type(None) + type(None), ) @@ -292,8 +292,7 @@ def _update_interdependencies(self): continue self._ensure_unique_parameter(setpoint_info, setpoint=True) - self.measurement.register_parameter( - setpoint_info["dataset_parameter"]) + self.measurement.register_parameter(setpoint_info["dataset_parameter"]) setpoint_info["registered"] = True # Register all measurement parameters in Measurement @@ -799,8 +798,7 @@ def _measure_callable( # No nested measurement has been performed in the callable. # Add results, which should be dict, by creating a nested measurement if not isinstance(results, dict): - raise SyntaxError( - f"{name} results must be a dict, not {results}") + raise SyntaxError(f"{name} results must be a dict, not {results}") with MeasurementLoop(name) as msmt: for key, val in results.items(): @@ -953,7 +951,7 @@ def measure( (t_now - self._t_start).total_seconds(), "T_pre", unit="s", - timestamp=False + timestamp=False, ) self.skip() # Increment last action index by 1 @@ -988,7 +986,7 @@ def measure( (t_now - self._t_start).total_seconds(), "T_post", unit="s", - timestamp=False + timestamp=False, ) self.skip() # Increment last action index by 1 @@ -1013,6 +1011,7 @@ def _mask_attr(self, obj: object, attr: str, value) -> Any: Returns: original value """ + original_value = getattr(obj, attr) setattr(obj, attr, value) @@ -1716,7 +1715,7 @@ def __init__( name=name, label=label, unit=unit, - revert=revert + revert=revert, ) sequence_kwargs, base_kwargs = self._transform_args_to_kwargs(*args, **kwargs) @@ -1864,7 +1863,9 @@ def _generate_sequence( # Use "parameter" current value if "start" is not provided if start is None: if parameter is None: - raise SyntaxError("Cannot use 'stop' without 'start' or a 'parameter'") + raise SyntaxError( + "Cannot use 'stop' without 'start' or a 'parameter'" + ) start = parameter() if start is None: raise ValueError( diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index d84fed87b0b..2d311607a43 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -214,7 +214,7 @@ def nested_measurement(): def test_measurement_no_parameter(): with MeasurementLoop("test") as msmt: for val in Sweep(np.linspace(0, 1, 11), "p1_set", label="p1 label", unit="V"): - msmt.measure(val+1, name="p1_get") + msmt.measure(val + 1, name="p1_get") data = msmt.dataset assert data.name == "test" diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index 3122ad4d796..8936a8b0cfb 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -23,6 +23,7 @@ def test_sweep_1_arg_sequence(): sweep = Sweep(sequence, name="sweep_name") assert sweep.sequence == sequence + def test_sweep_1_arg_parameter_stop(): sweep_parameter = ManualParameter("sweep_parameter") @@ -126,6 +127,7 @@ def test_sweep_len(): sweep = Sweep(start=0, stop=10, step=0.5) assert len(sweep) == 21 + def test_error_on_iterate_sweep(): sweep = Sweep([1, 2, 3], "sweep") diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index 9677eab51d8..bdc092115df 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -112,10 +112,10 @@ def get_exponent(val): (0, ""), (-3, "m"), (-6, "u"), - (-9, "n") + (-9, "n"), ] for exponent, prefactor in prefactors: - if val >= np.power(10., exponent): + if val >= np.power(10.0, exponent): return exponent, prefactor else: return prefactors[-1] From 369c5dccccb970a6c66e66f358ae195db6e1f4ac Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 14 Sep 2022 10:02:51 +0200 Subject: [PATCH 068/122] typing fixes --- qcodes/dataset/measurement_loop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 7cabf17a51d..29fbf16e1a1 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1921,8 +1921,8 @@ def __init__( def measure_sweeps( - sweeps: list[BaseSweep], - measure_params: list[_BaseParameter], + sweeps: List[BaseSweep], + measure_params: List[_BaseParameter], msmt: "MeasurementLoop" = None, ): """Recursively iterate over Sweep objects, measuring measure_params in innermost loop From 54d92971c473f818016ad681f47eebcc4daa69c6 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 14 Sep 2022 10:04:35 +0200 Subject: [PATCH 069/122] return self in MeasurementLoop context manager --- qcodes/dataset/measurement_loop.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 29fbf16e1a1..817b375c5ad 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -535,6 +535,8 @@ def __enter__(self): self.data_arrays = msmt.data_arrays self.set_arrays = msmt.set_arrays self.timings = msmt.timings + + return self except: # An error has occured, ensure running_measurement is cleared if MeasurementLoop.running_measurement is self: From ae10757d7a6b337d42384c1ab9229bb788e59f7f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 14 Sep 2022 08:05:10 +0000 Subject: [PATCH 070/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 817b375c5ad..ff332869853 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -535,7 +535,7 @@ def __enter__(self): self.data_arrays = msmt.data_arrays self.set_arrays = msmt.set_arrays self.timings = msmt.timings - + return self except: # An error has occured, ensure running_measurement is cleared From 66a7a63327aa31d87f92ca8828d29a876eeecc66 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 14 Sep 2022 10:21:47 +0200 Subject: [PATCH 071/122] Fixed last pep issue? --- qcodes/dataset/measurement_loop.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 817b375c5ad..bc7e2d1b191 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1672,6 +1672,7 @@ class Sweep(BaseSweep): revert: Revert parameter back to original value after the sweep ends. This is False by default, unless the kwarg ``around`` is passed """ + sequence_keywords = [ "start", "stop", From 800f0f61d1fa51d92fbfdac431f18fe99b884f74 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 14 Sep 2022 10:22:06 +0200 Subject: [PATCH 072/122] Fixing Keysight capitalization issue --- qcodes/instrument_drivers/Keysight/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 qcodes/instrument_drivers/Keysight/__init__.py diff --git a/qcodes/instrument_drivers/Keysight/__init__.py b/qcodes/instrument_drivers/Keysight/__init__.py new file mode 100644 index 00000000000..e484f8b84cd --- /dev/null +++ b/qcodes/instrument_drivers/Keysight/__init__.py @@ -0,0 +1 @@ +# Intentionally left blank From f1f2067586f69317234e13173fc13b3e686f78d5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 14 Sep 2022 08:22:44 +0000 Subject: [PATCH 073/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 197a60564dc..a9b7cfb3588 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1672,7 +1672,7 @@ class Sweep(BaseSweep): revert: Revert parameter back to original value after the sweep ends. This is False by default, unless the kwarg ``around`` is passed """ - + sequence_keywords = [ "start", "stop", From 74596eb3c7fb3e69f10a2e57ae9f50cdd83352ca Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 14 Sep 2022 10:23:02 +0200 Subject: [PATCH 074/122] Delete __init__.py --- qcodes/instrument_drivers/keysight/__init__.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 qcodes/instrument_drivers/keysight/__init__.py diff --git a/qcodes/instrument_drivers/keysight/__init__.py b/qcodes/instrument_drivers/keysight/__init__.py deleted file mode 100644 index e484f8b84cd..00000000000 --- a/qcodes/instrument_drivers/keysight/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Intentionally left blank From 33cac932800f7f7a8059847b5df465028222c5eb Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 14 Sep 2022 10:45:17 +0200 Subject: [PATCH 075/122] fixing pytest issues --- qcodes/dataset/data_set_protocol.py | 1 + qcodes/dataset/measurement_loop.py | 71 ++++++++++++++--------------- qcodes/utils/helpers.py | 62 +++---------------------- 3 files changed, 40 insertions(+), 94 deletions(-) diff --git a/qcodes/dataset/data_set_protocol.py b/qcodes/dataset/data_set_protocol.py index 67a35de0955..653d900ffc4 100644 --- a/qcodes/dataset/data_set_protocol.py +++ b/qcodes/dataset/data_set_protocol.py @@ -88,6 +88,7 @@ def prepare( shapes: Shapes = None, parent_datasets: Sequence[Mapping[Any, Any]] = (), write_in_background: bool = False, + allow_empty_dataset: bool = False, ) -> None: pass diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 197a60564dc..2ae5c6c8805 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -28,12 +28,7 @@ from qcodes.parameters import ParameterBase from qcodes.station import Station from qcodes.utils.dataset.doNd import AbstractSweep, ActionsT -from qcodes.utils.helpers import ( - PerformanceTimer, - directly_executed_from_cell, - get_last_input_cells, - using_ipython, -) +from qcodes.utils.helpers import PerformanceTimer RAW_VALUE_TYPES = ( float, @@ -53,15 +48,15 @@ class _DatasetHandler: Used by the `MeasurementLoop` as an interface to the `Measurement` and `DataSet` """ - def __init__(self, measurement_loop: "MeasurementLoop", name="results"): + def __init__(self, measurement_loop: "MeasurementLoop", name: str = "results"): self.measurement_loop = measurement_loop self.name = name self.initialized: bool = False - self.datasaver: DataSaver = None - self.runner: Runner = None - self.measurement: Measurement = None - self.dataset: DataSetProtocol = None + self.datasaver: Optional[DataSaver] = None + self.runner: Optional[Runner] = None + self.measurement: Optional[Measurement] = None + self.dataset: Optional[DataSetProtocol] = None # Key: action_index # Values: @@ -77,11 +72,11 @@ def __init__(self, measurement_loop: "MeasurementLoop", name="results"): # - shape # - unstored_results - list where each element contains (*setpoints, measurement_value) # - latest_value - self.measurement_list: Dict[str, Any] = dict() + self.measurement_list: Dict[Tuple[int], Any] = dict() self.initialize() - def initialize(self): + def initialize(self) -> None: """Creates a `Measurement`, runs it and initializes a dataset""" # Once initialized, no new parameters can be added assert not self.initialized, "Cannot initialize twice" @@ -98,13 +93,13 @@ def initialize(self): self.initialized = True - def finalize(self): + def finalize(self) -> None: """Finishes a measurement by flushing all data to the database""" self.datasaver.flush_data_to_database() def _ensure_unique_parameter( self, parameter_info: dict, setpoint: bool, max_idx: int = 99 - ): + ) -> None: """Ensure setpoint / measurement parameters have unique names If a previously registered parameter already shares the same name, it adds a @@ -152,7 +147,7 @@ def _ensure_unique_parameter( def create_measurement_info( self, action_indices: Tuple[int], - parameter: _BaseParameter, + parameter: Parameter, name: Optional[str] = None, label: Optional[str] = None, unit: Optional[str] = None, @@ -204,7 +199,7 @@ def register_new_measurement( name: Optional[str] = None, label: Optional[str] = None, unit: Optional[str] = None, - ): + ) -> None: """Register a new measurement parameter""" measurement_info = self.create_measurement_info( action_indices=action_indices, @@ -226,7 +221,7 @@ def add_measurement_result( name: Optional[str] = None, label: Optional[str] = None, unit: Optional[str] = None, - ): + ) -> None: """Store single measurement result This method is called from type-specific methods, such as @@ -273,7 +268,7 @@ def add_measurement_result( # Also store in measurement_info measurement_info["latest_value"] = result - def _update_interdependencies(self): + def _update_interdependencies(self) -> None: """Updates dataset after instantiation to include new setpoint/measurement parameter The `DataSet` was not made to register parameters after instantiation, so this @@ -429,7 +424,7 @@ def __init__(self, name: Optional[str], notify: bool = False): def dataset(self) -> DataSetProtocol: return self.data_handler.dataset - def log(self, message: str, level: str = "info"): + def log(self, message: str, level: str = "info") -> None: """Send a log message Args: @@ -475,7 +470,7 @@ def measurement_list(self) -> Optional[Dict[Tuple[int], Any]]: else: return None - def __enter__(self): + def __enter__(self) -> "MeasurementLoop": """Operation when entering a loop, including dataset instantiation""" self.is_context_manager = True @@ -543,7 +538,7 @@ def __enter__(self): MeasurementLoop.running_measurement = None raise - def __exit__(self, exc_type: Exception, exc_val, exc_tb): + def __exit__(self, exc_type: Exception, exc_val, exc_tb) -> None: """Operation when exiting a loop Args: @@ -632,7 +627,7 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb): # } # ) - def _verify_action(self, action: Callable, name: str, add_if_new: bool = True): + def _verify_action(self, action: Callable, name: str, add_if_new: bool = True) -> None: """Verify an action corresponds to the current action indices. An action is usually (currently always) a measurement. @@ -657,7 +652,7 @@ def _verify_action(self, action: Callable, name: str, add_if_new: bool = True): f"Expected: {self.action_names[self.action_indices]}. Received: {name}" ) - def _apply_actions(self, actions: list, label="", clear=False): + def _apply_actions(self, actions: list, label="", clear=False) -> None: """Apply actions, either except_actions or final_actions""" for action in actions: try: @@ -890,7 +885,7 @@ def measure( unit: Optional[str] = None, timestamp: bool = False, **kwargs, - ): + )-> Any: """Perform a single measurement of a Parameter, function, etc. @@ -1147,7 +1142,7 @@ def unmask( value: Optional[Any] = None, raise_exception: bool = True, **kwargs, # Add kwargs because original_value may be None - ): + ) -> None: """Unmasks a previously masked object, i.e. revert value back to original Args: @@ -1200,7 +1195,7 @@ def unmask( if raise_exception: raise e - def unmask_all(self): + def unmask_all(self) -> None: """Unmask all masked properties""" masked_properties = reversed(self._masked_properties) for masked_property in masked_properties: @@ -1208,15 +1203,15 @@ def unmask_all(self): self._masked_properties.clear() # Functions relating to measurement flow - def pause(self): + def pause(self) -> None: """Pause measurement at start of next parameter sweep/measurement""" running_measurement().is_paused = True - def resume(self): + def resume(self) -> None: """Resume measurement after being paused""" running_measurement().is_paused = False - def stop(self): + def stop(self) -> None: """Stop measurement at start of next parameter sweep/measurement""" running_measurement().is_stopped = True # Unpause loop @@ -1259,7 +1254,7 @@ def skip(self, N: int = 1) -> Tuple[int]: self.action_indices = tuple(action_indices) return self.action_indices - def step_out(self, reduce_dimension: bool = True): + def step_out(self, reduce_dimension: bool = True) -> None: """Step out of a Sweep This function usually doesn't need to be called. @@ -1278,7 +1273,7 @@ def step_out(self, reduce_dimension: bool = True): action_indices[-1] += 1 self.action_indices = tuple(action_indices) - def traceback(self): + def traceback(self) -> None: """Print traceback if an error occurred. Measurement must be ran from separate thread @@ -1309,7 +1304,7 @@ def __iter__(self) -> Iterable: self.iterator = iter(self.sweep.get_setpoints()) return self - def __next__(self): + def __next__(self) -> float: value = next(self.iterator) self.sweep._param(value) @@ -1522,7 +1517,7 @@ def initialize(self) -> Dict[str, Any]: return setpoint_info - def exit_sweep(self): + def exit_sweep(self) -> None: """Exits sweep, stepping out of the current `Measurement.action_indices`""" msmt = running_measurement() msmt.step_out(reduce_dimension=True) @@ -1672,7 +1667,7 @@ class Sweep(BaseSweep): revert: Revert parameter back to original value after the sweep ends. This is False by default, unless the kwarg ``around`` is passed """ - + sequence_keywords = [ "start", "stop", @@ -1727,7 +1722,7 @@ def __init__( super().__init__(sequence=self.sequence, **base_kwargs) - def _transform_args_to_kwargs(self, *args, **kwargs): + def _transform_args_to_kwargs(self, *args, **kwargs) -> Tuple[dict]: """Transforms sweep initialization args to kwargs. Allowed args are: @@ -1839,7 +1834,7 @@ def _generate_sequence( step: Optional[float] = None, parameter: Optional[_BaseParameter] = None, sequence: Optional[Iterable] = None, - ): + ) -> Sequence: """Creates a sequence from passed values""" # Return "sequence" if explicitly provided if sequence is not None: @@ -1927,7 +1922,7 @@ def measure_sweeps( sweeps: List[BaseSweep], measure_params: List[_BaseParameter], msmt: "MeasurementLoop" = None, -): +) -> None: """Recursively iterate over Sweep objects, measuring measure_params in innermost loop This method is used to perform arbitrary-dimension by passing a list of sweeps, diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index bdc092115df..89151a37c35 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -4,6 +4,7 @@ """ import logging from contextlib import contextmanager +from typing import Any # for backwards compatibility since this module used # to contain logic that would abstract between yaml @@ -53,58 +54,7 @@ def warn_units(class_name: str, instance: object) -> None: from qcodes.configuration.config import DotDict -def using_ipython() -> bool: - """Check if code is run from IPython (including jupyter notebook/lab)""" - return hasattr(builtins, "__IPYTHON__") - - -def directly_executed_from_cell(level: int = 1) -> bool: - """Test if this function is called directly from an IPython cell - The IPython prompt is also valid. - - Args: - level: Difference in frames from IPython cell/prompt to check. - Since the check is executed from this function, the default level is 1. - - Returns: - True if directly run from IPython cell/prompt, False otherwise - - Examples: - These examples should be run in a notebook cell. - - >>> directly_executed_from_cell() - ... True - - >>> def wrap_function(**kwargs): - >>> return directly_executed_from_cell(**kwargs) - >>> wrap_function() - ... False - >>> wrap_function(level=2) - ... True - - """ - if level < 1: - raise SyntaxError("Level must be 1 or higher") - - frame = sys._getframe(level) - return "_" in frame.f_locals - - -def get_last_input_cells(cells=3): - """ - Get last input cell. Note that get_last_input_cell.globals must be set to - the ipython globals - Returns: - last cell input if successful, else None - """ - global In - if "In" in globals() or hasattr(builtins, "In"): - return In[-cells:] - else: - logging.warning("No input cells found") - - -def get_exponent(val): +def get_exponent(val: float): prefactors = [ (9, "G"), (6, "M"), @@ -127,7 +77,7 @@ class PerformanceTimer: def __init__(self): self.timings = DotDict() - def __getitem__(self, key): + def __getitem__(self, key: str): val = self.timings.__getitem__(key) return self._timing_to_str(val) @@ -137,14 +87,14 @@ def __repr__(self): def clear(self): self.timings.clear() - def _timing_to_str(self, val): + def _timing_to_str(self, val: float) -> str: mean_val = np.mean(val) exponent, prefactor = get_exponent(mean_val) factor = np.power(10.0, exponent) return f"{mean_val / factor:.3g}+-{np.abs(np.std(val))/factor:.3g} {prefactor}s" - def _timings_to_str(self, d: dict): + def _timings_to_str(self, d: dict) -> str: timings_str = DotDict() for key, val in d.items(): @@ -156,7 +106,7 @@ def _timings_to_str(self, d: dict): return timings_str @contextmanager - def record(self, key, val=None): + def record(self, key: str, val: Any = None): if isinstance(key, str): timing_list = self.timings.setdefault(key, []) elif isinstance(key, (list)): From ae78eedc18515b71f0fc38ff3c396d3a86495864 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 14 Sep 2022 11:01:48 +0200 Subject: [PATCH 076/122] Fixing pytest issues --- qcodes/dataset/measurement_loop.py | 68 +++++++++---------- .../test_measurement_loop_basics.py | 22 ++---- .../test_measurement_loop_sweep.py | 9 --- qcodes/utils/helpers.py | 4 +- 4 files changed, 41 insertions(+), 62 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 2ae5c6c8805..39ad8b8b298 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1,14 +1,12 @@ import logging import threading import traceback -from ast import Call from datetime import datetime from time import perf_counter, sleep from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union import numpy as np -from qcodes import config as qcodes_config from qcodes.dataset.data_set_protocol import DataSetProtocol from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement from qcodes.dataset.descriptions.rundescriber import RunDescriber @@ -72,7 +70,7 @@ def __init__(self, measurement_loop: "MeasurementLoop", name: str = "results"): # - shape # - unstored_results - list where each element contains (*setpoints, measurement_value) # - latest_value - self.measurement_list: Dict[Tuple[int], Any] = dict() + self.measurement_list: Dict[Tuple[int], Any] = {} self.initialize() @@ -575,18 +573,16 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb) -> None: if self.notify and self.notify_function is not None: try: self.notify_function(exc_type, exc_val, exc_tb) - except: + except Exception: self.log("Could not notify", level="error") - t_stop = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - # TODO include metadata + # t_stop = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # self.data_handler.add_metadata({"t_stop": t_stop}) # self.data_handler.add_metadata({"timings": self.timings}) self.data_handler.finalize() - self.log(f"Measurement finished") - + self.log("Measurement finished") else: msmt.step_out(reduce_dimension=False) @@ -617,7 +613,7 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb) -> None: # if measurement_code.startswith(init_string): # measurement_code = measurement_code[len(init_string) + 1 : -4] - # self._t_start = datetime.now() + self._t_start = datetime.now() # dataset.add_metadata( # { # "measurement_cell": measurement_cell, @@ -657,7 +653,7 @@ def _apply_actions(self, actions: list, label="", clear=False) -> None: for action in actions: try: action() - except Exception as e: + except Exception: self.log( f"Could not execute {label} action {action} \n" f"{traceback.format_exc()}", @@ -754,7 +750,7 @@ def _measure_multi_parameter( return results def _measure_callable( - self, callable: Callable, name: str = None, **kwargs + self, measurable_function: Callable, name: str = None, **kwargs ) -> Dict[str, Any]: """Measure a callable (function) and store results @@ -768,23 +764,23 @@ def _measure_callable( """ # Determine name if name is None: - if hasattr(callable, "__self__") and isinstance( - callable.__self__, InstrumentBase + if hasattr(measurable_function, "__self__") and isinstance( + measurable_function.__self__, InstrumentBase ): - name = callable.__self__.name - elif hasattr(callable, "__name__"): - name = callable.__name__ + name = measurable_function.__self__.name + elif hasattr(measurable_function, "__name__"): + name = measurable_function.__name__ else: action_indices_str = "_".join(str(idx) for idx in self.action_indices) name = f"data_group_{action_indices_str}" # Ensure measuring callable matches the current action_indices - self._verify_action(action=callable, name=name, add_if_new=True) + self._verify_action(action=measurable_function, name=name, add_if_new=True) # Record action_indices before the callable is called action_indices = self.action_indices - results = callable(**kwargs) + results = measurable_function(**kwargs) # Check if the callable already performed a nested measurement # In this case, the nested measurement is stored as a data_group, and @@ -914,9 +910,9 @@ def measure( "Must use the Measurement as a context manager, " "i.e. 'with Measurement(name) as msmt:'" ) - elif self.is_stopped: + if self.is_stopped: raise SystemExit("Measurement.stop() has been called") - elif threading.current_thread() is not MeasurementLoop.measurement_thread: + if threading.current_thread() is not MeasurementLoop.measurement_thread: raise RuntimeError( "Cannot measure while another measurement is already running " "in a different thread." @@ -1138,7 +1134,7 @@ def unmask( obj: Union[_BaseParameter, object, dict], attr: Optional[str] = None, key: Optional[str] = None, - type: Optional[str] = None, + unmask_type: Optional[str] = None, value: Optional[Any] = None, raise_exception: bool = True, **kwargs, # Add kwargs because original_value may be None @@ -1176,17 +1172,17 @@ def unmask( # A masked property has been passed, which we unmask here try: original_value = kwargs["original_value"] - if type == "key": + if unmask_type == "key": obj[key] = original_value - elif type == "attr": + elif unmask_type == "attr": setattr(obj, attr, original_value) - elif type == "parameter": + elif unmask_type == "parameter": obj(original_value) else: - raise SyntaxError(f"Unmask type {type} not understood") + raise SyntaxError(f"Unmask type {unmask_type} not understood") except Exception as e: self.log( - f"Could not unmask {obj} {type} from masked value {value} " + f"Could not unmask {obj} {unmask_type} from masked value {value} " f"to original value {original_value}\n" f"{traceback.format_exc()}", level="error", @@ -1280,8 +1276,8 @@ def traceback(self) -> None: """ if self.measurement_thread is None: raise RuntimeError("Measurement was not started in separate thread") - else: - self.measurement_thread.traceback() + + self.measurement_thread.traceback() def running_measurement() -> MeasurementLoop: @@ -1442,7 +1438,8 @@ def __next__(self) -> Any: "Must use the Measurement as a context manager, " "i.e. 'with Measurement(name) as msmt:'" ) - elif msmt.is_stopped: + + if msmt.is_stopped: raise SystemExit # Wait as long as the measurement is paused @@ -1641,7 +1638,7 @@ class Sweep(BaseSweep): - Sweep(parameter, start_val, stop_val) : sweep "parameter" from "start_val" to "stop_val" If "num" or "step" is not given as kwarg, it will check if "num" or "step" - if set in dict "parameter.sweep_defaults" and use that, or raise an error otherwise. + is set in dict "parameter.sweep_defaults" and use that, or else raise an error. 4 args: - Sweep(parameter, start_val, stop_val, num) : Sweep "parameter" from "start_val" to "stop_val" with "num" number of points @@ -1652,9 +1649,9 @@ class Sweep(BaseSweep): stop: stop value of sweep sequence Cannot be used together with ``around`` around: sweep around the current parameter value. - ``start`` and ``stop`` are then defined from ``around`` and the current vlaue + ``start`` and ``stop`` are defined from ``around`` and the current value i.e. start=X-dx, stop=X+dx when current_value=X and around=dx. - Passing the kwarg "around" also sets revert=True unless explicitly set to False. + Passing the kwarg "around" also sets revert=True unless explicitly set False num: Number of points between start and stop. Cannot be used together with ``step`` step: Increment from start to stop. @@ -1774,8 +1771,8 @@ def _transform_args_to_kwargs(self, *args, **kwargs) -> Tuple[dict]: kwargs["parameter"], kwargs["stop"] = args else: raise SyntaxError( - "Sweep with Parameter arg and second arg should have second arg be either " - "a sequence or a target value" + "Sweep with Parameter arg and second arg should have second arg + " be either a sequence or a target value" ) elif isinstance(args[0], Iterable): # Sweep([1,2,3], "name") assert isinstance(args[1], str) @@ -1885,7 +1882,8 @@ def _generate_sequence( sequence = np.append(sequence, [stop]) else: raise SyntaxError( - "Cannot determine measurement points. Either provide 'sequence', 'step' or 'num'" + "Cannot determine measurement points. " + "Either provide 'sequence', 'step' or 'num'" ) return sequence diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 2d311607a43..12c42a1258c 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -1,21 +1,11 @@ -import contextlib -import shutil -import tempfile -from pathlib import Path - import numpy as np import pytest from qcodes.dataset import ( - LinSweep, MeasurementLoop, Sweep, - dond, - initialise_or_create_database_at, - load_by_id, - load_or_create_experiment, ) -from qcodes.instrument import ManualParameter, Parameter +from qcodes.instrument import ManualParameter @pytest.mark.usefixtures("empty_temp_db", "experiment") @@ -32,7 +22,7 @@ def test_create_measurement(): MeasurementLoop("test") -def test_basic_1D_measurement(): +def test_basic_1d_measurement(): # Initialize parameters p1_get = ManualParameter("p1_get") p1_set = ManualParameter("p1_set") @@ -54,7 +44,7 @@ def test_basic_1D_measurement(): assert np.allclose(data_arrays["p1_set"], np.linspace(0, 1, 11)) -def test_basic_2D_measurement(): +def test_basic_2d_measurement(): # Initialize parameters p1_get = ManualParameter("p1_get") p1_set = ManualParameter("p1_set") @@ -86,7 +76,7 @@ def test_basic_2D_measurement(): ) -def test_1D_measurement_duplicate_get(): +def test_1d_measurement_duplicate_get(): # Initialize parameters p1_get = ManualParameter("p1_get") p1_set = ManualParameter("p1_set") @@ -113,7 +103,7 @@ def test_1D_measurement_duplicate_get(): assert np.allclose(data_arrays["p1_set"], np.linspace(0, 1, 11)) -def test_1D_measurement_duplicate_getset(): +def test_1d_measurement_duplicate_getset(): # Initialize parameters p1_get = ManualParameter("p1_get") p1_set = ManualParameter("p1_set") @@ -146,7 +136,7 @@ def test_1D_measurement_duplicate_getset(): assert np.allclose(data_arrays[set_key], np.linspace(0, 1, 11)) -def test_2D_measurement_initialization(): +def test_2d_measurement_initialization(): # Initialize parameters p1_get = ManualParameter("p1_get") p1_set = ManualParameter("p1_set") diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index 8936a8b0cfb..2f2c8e0a6fb 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -1,19 +1,10 @@ -import contextlib -import shutil -import tempfile -from pathlib import Path - import numpy as np import pytest from qcodes.dataset import ( LinSweep, - MeasurementLoop, Sweep, dond, - initialise_or_create_database_at, - load_by_id, - load_or_create_experiment, ) from qcodes.instrument import ManualParameter, Parameter diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index 89151a37c35..8829de44484 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -67,8 +67,8 @@ def get_exponent(val: float): for exponent, prefactor in prefactors: if val >= np.power(10.0, exponent): return exponent, prefactor - else: - return prefactors[-1] + + return prefactors[-1] class PerformanceTimer: From 93027b93885cc4ca6daaa74506cc102f33ed2732 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 14 Sep 2022 09:08:21 +0000 Subject: [PATCH 077/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 4 ++-- .../measurement_loop/test_measurement_loop_basics.py | 5 +---- .../dataset/measurement_loop/test_measurement_loop_sweep.py | 6 +----- qcodes/utils/helpers.py | 2 +- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 39ad8b8b298..fb9fc39b85b 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1276,7 +1276,7 @@ def traceback(self) -> None: """ if self.measurement_thread is None: raise RuntimeError("Measurement was not started in separate thread") - + self.measurement_thread.traceback() @@ -1438,7 +1438,7 @@ def __next__(self) -> Any: "Must use the Measurement as a context manager, " "i.e. 'with Measurement(name) as msmt:'" ) - + if msmt.is_stopped: raise SystemExit diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 12c42a1258c..f85f5f1c31a 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -1,10 +1,7 @@ import numpy as np import pytest -from qcodes.dataset import ( - MeasurementLoop, - Sweep, -) +from qcodes.dataset import MeasurementLoop, Sweep from qcodes.instrument import ManualParameter diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index 2f2c8e0a6fb..401d4e2840b 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -1,11 +1,7 @@ import numpy as np import pytest -from qcodes.dataset import ( - LinSweep, - Sweep, - dond, -) +from qcodes.dataset import LinSweep, Sweep, dond from qcodes.instrument import ManualParameter, Parameter diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index 8829de44484..dec4faeff50 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -67,7 +67,7 @@ def get_exponent(val: float): for exponent, prefactor in prefactors: if val >= np.power(10.0, exponent): return exponent, prefactor - + return prefactors[-1] From 9c587e5763049ac04b846459855d3a79f5e44a34 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 14 Sep 2022 11:09:38 +0200 Subject: [PATCH 078/122] missing double quote --- qcodes/dataset/measurement_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 39ad8b8b298..6b94ccf64fa 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1771,7 +1771,7 @@ def _transform_args_to_kwargs(self, *args, **kwargs) -> Tuple[dict]: kwargs["parameter"], kwargs["stop"] = args else: raise SyntaxError( - "Sweep with Parameter arg and second arg should have second arg + "Sweep with Parameter arg and second arg should have second arg" " be either a sequence or a target value" ) elif isinstance(args[0], Iterable): # Sweep([1,2,3], "name") From e0990710d73808655ce304fbadea7e96541eb830 Mon Sep 17 00:00:00 2001 From: Serwan Date: Thu, 15 Sep 2022 09:41:16 +0200 Subject: [PATCH 079/122] allow linsweep in MeasurementLoop --- qcodes/dataset/measurement_loop.py | 6 ++++++ .../test_measurement_loop_sweep.py | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index f4a1b0340d7..866f393ca82 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1759,6 +1759,12 @@ def _transform_args_to_kwargs(self, *args, **kwargs) -> Tuple[dict]: kwargs.get("stop") is not None or kwargs.get("around") is not None ), "Must provide stop value for parameter" (kwargs["parameter"],) = args + elif isinstance(args[0], AbstractSweep): + kwargs["sequence"] = _IterateDondSweep(args[0]) + parameter = kwargs["sequence"].parameter + kwargs["name"] = kwargs["name"] or parameter.name + kwargs["label"] = kwargs["label"] or parameter.label + kwargs["unit"] = kwargs["unit"] or parameter.unit else: raise SyntaxError( "Sweep with 1 arg must have iterable or parameter as arg" diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index 401d4e2840b..61bc3ab498a 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from qcodes.dataset import LinSweep, Sweep, dond +from qcodes.dataset import LinSweep, Sweep, dond, MeasurementLoop from qcodes.instrument import ManualParameter, Parameter @@ -150,6 +150,21 @@ def test_sweep_and_linsweep_in_dond(): assert np.allclose(arr, np.repeat(np.array([1, 2, 3])[:, np.newaxis], 11, axis=1)) +@pytest.mark.usefixtures("empty_temp_db", "experiment") +def test_linsweep_in_MeasurementLoop(): + set_parameter = ManualParameter("set_param") + get_parameter = ManualParameter("get_param", initial_value=42) + + linsweep = LinSweep(set_parameter, 0, 10, 11) + + sweep = Sweep(linsweep) + assert sweep.name == 'set_param' + + with MeasurementLoop('linsweep_in_MeasurementLoop') as msmt: + for k, val in enumerate(sweep): + assert val == k + msmt.measure(get_parameter) + def test_sweep_execute_sweep_args(): set_parameter = ManualParameter("set_param") sweep = Sweep(set_parameter, [1, 2, 3]) From 448acc07627cc798c9bcc65dabb9e55d29617585 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 15 Sep 2022 07:41:42 +0000 Subject: [PATCH 080/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../dataset/measurement_loop/test_measurement_loop_sweep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index 61bc3ab498a..50113eabb27 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from qcodes.dataset import LinSweep, Sweep, dond, MeasurementLoop +from qcodes.dataset import LinSweep, MeasurementLoop, Sweep, dond from qcodes.instrument import ManualParameter, Parameter From 16c7f09f425de7afa7a357727d23db7f8170437c Mon Sep 17 00:00:00 2001 From: Serwan Date: Thu, 15 Sep 2022 09:59:33 +0200 Subject: [PATCH 081/122] fix type errors --- qcodes/dataset/measurement_loop.py | 8 +++-- .../test_measurement_loop_sweep.py | 5 +-- qcodes/utils/helpers.py | 34 +++++++++++++++---- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 866f393ca82..7aa11de35ec 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -587,6 +587,7 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb) -> None: msmt.step_out(reduce_dimension=False) self.is_context_manager = False + self._t_start = datetime.now() # TODO Needs to be implemented # def _initialize_metadata(self, dataset): @@ -613,7 +614,6 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb) -> None: # if measurement_code.startswith(init_string): # measurement_code = measurement_code[len(init_string) + 1 : -4] - self._t_start = datetime.now() # dataset.add_metadata( # { # "measurement_cell": measurement_cell, @@ -623,7 +623,9 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb) -> None: # } # ) - def _verify_action(self, action: Callable, name: str, add_if_new: bool = True) -> None: + def _verify_action( + self, action: Callable, name: str, add_if_new: bool = True + ) -> None: """Verify an action corresponds to the current action indices. An action is usually (currently always) a measurement. @@ -881,7 +883,7 @@ def measure( unit: Optional[str] = None, timestamp: bool = False, **kwargs, - )-> Any: + ) -> Any: """Perform a single measurement of a Parameter, function, etc. diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index 61bc3ab498a..d64f4427c4f 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -158,13 +158,14 @@ def test_linsweep_in_MeasurementLoop(): linsweep = LinSweep(set_parameter, 0, 10, 11) sweep = Sweep(linsweep) - assert sweep.name == 'set_param' + assert sweep.name == "set_param" - with MeasurementLoop('linsweep_in_MeasurementLoop') as msmt: + with MeasurementLoop("linsweep_in_MeasurementLoop") as msmt: for k, val in enumerate(sweep): assert val == k msmt.measure(get_parameter) + def test_sweep_execute_sweep_args(): set_parameter = ManualParameter("set_param") sweep = Sweep(set_parameter, [1, 2, 3]) diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index dec4faeff50..9e5f8763b4b 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -4,7 +4,7 @@ """ import logging from contextlib import contextmanager -from typing import Any +from typing import Any, Tuple # for backwards compatibility since this module used # to contain logic that would abstract between yaml @@ -54,7 +54,26 @@ def warn_units(class_name: str, instance: object) -> None: from qcodes.configuration.config import DotDict -def get_exponent(val: float): +def get_exponent_prefactor(val: float) -> Tuple[int, str]: + """Get the exponent and unit prefactor of a number + + Currently lower bounded at atto + + Args: + val: value for which to get exponent and prefactor + + Returns: + Exponent corresponding to prefactor + Prefactor + + Examples: + ``` + get_exponent_prefactor(1.82e-8) + >>> -9, "n" # i.e. 18.2*10**-9 n{unit} + ``` + + + """ prefactors = [ (9, "G"), (6, "M"), @@ -63,6 +82,9 @@ def get_exponent(val: float): (-3, "m"), (-6, "u"), (-9, "n"), + (-12, "p"), + (-15, "f"), + (-18, "a"), ] for exponent, prefactor in prefactors: if val >= np.power(10.0, exponent): @@ -77,19 +99,19 @@ class PerformanceTimer: def __init__(self): self.timings = DotDict() - def __getitem__(self, key: str): + def __getitem__(self, key: str) -> str: val = self.timings.__getitem__(key) return self._timing_to_str(val) def __repr__(self): return pprint.pformat(self._timings_to_str(self.timings), indent=2) - def clear(self): + def clear(self) -> None: self.timings.clear() def _timing_to_str(self, val: float) -> str: mean_val = np.mean(val) - exponent, prefactor = get_exponent(mean_val) + exponent, prefactor = get_exponent_prefactor(mean_val) factor = np.power(10.0, exponent) return f"{mean_val / factor:.3g}+-{np.abs(np.std(val))/factor:.3g} {prefactor}s" @@ -106,7 +128,7 @@ def _timings_to_str(self, d: dict) -> str: return timings_str @contextmanager - def record(self, key: str, val: Any = None): + def record(self, key: str, val: Any = None) -> None: if isinstance(key, str): timing_list = self.timings.setdefault(key, []) elif isinstance(key, (list)): From 3d85b550a47065253886ce11c4333e2de2bf7364 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 15 Sep 2022 08:00:01 +0000 Subject: [PATCH 082/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/utils/helpers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index 9e5f8763b4b..b36f2a029be 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -56,12 +56,12 @@ def warn_units(class_name: str, instance: object) -> None: def get_exponent_prefactor(val: float) -> Tuple[int, str]: """Get the exponent and unit prefactor of a number - + Currently lower bounded at atto Args: val: value for which to get exponent and prefactor - + Returns: Exponent corresponding to prefactor Prefactor @@ -71,7 +71,7 @@ def get_exponent_prefactor(val: float) -> Tuple[int, str]: get_exponent_prefactor(1.82e-8) >>> -9, "n" # i.e. 18.2*10**-9 n{unit} ``` - + """ prefactors = [ From b2e0537f525fb71b1a681af3b937eb70a25c0909 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 20 Sep 2022 18:06:31 +0200 Subject: [PATCH 083/122] fraction complete working for basic example --- qcodes/dataset/measurement_loop.py | 94 ++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 7aa11de35ec..b1e7079a02c 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -665,6 +665,100 @@ def _apply_actions(self, actions: list, label="", clear=False) -> None: if clear: actions.clear() + def _get_maximum_action_index(self, action_indices, position): + msmt = running_measurement() + + # Get maximum action idx + max_idx = 0 + for idxs in msmt.actions: + if idxs[:position] != action_indices[:position]: + continue + if len(idxs) <= position: + continue + max_idx = max(max_idx, idxs[position]) + return max_idx + + def _fraction_complete_action_indices(self, action_indices, silent=True): + """Calculate fraction complete from finished action_indices""" + msmt = running_measurement() + fraction_complete = 0 + scale = 1 + + max_idxs = [] + for k, action_idx in enumerate(action_indices): + # Check if previous idx is a sweep + # If so, reduce scale by loop dimension + action = msmt.actions.get(action_indices[:k]) + if not silent: + print(f'{action=}, {isinstance(action, BaseSweep)=}') + if isinstance(action, BaseSweep): + if not silent: + print(f'Decreasing scale by {len(action)}') + scale /= len(action) + + max_idx = self._get_maximum_action_index(action_indices, position=k) + + fraction_complete += action_idx / (max_idx + 1) * scale + scale /= max_idx + 1 + max_idxs.append(max_idx) + if not silent: + print(f'{fraction_complete=}, {scale=}, {action_idx=}, {max_idxs=}') + + return fraction_complete + + def _fraction_complete_loop(self, action_indices, silent=True): + msmt = running_measurement() + fraction_complete = 0 + scale = 1 + loop_idx = 0 + + for k, action_idx in enumerate(action_indices): + # Check if current action is a sweep + # If so, reduce scale by action index fraction + action = msmt.actions.get(action_indices[:k+1]) + if isinstance(action, BaseSweep): + max_idx = self._get_maximum_action_index(action_indices, position=k) + + if not silent: + print(f'Reducing current Sweep {loop_idx=} {msmt.loop_indices[loop_idx]} / {len(action)} * {scale}') + print(f'{max_idx=}') + scale /= (max_idx + 1) + + # Check if previous idx is a sweep + # If so, reduce scale by loop dimension + action = msmt.actions.get(action_indices[:k]) + if not silent: + print(f'{action=}, {isinstance(action, BaseSweep)=}') + if isinstance(action, BaseSweep): + if not silent: + print(f'Reducing previous Sweep {loop_idx=} fraction {msmt.loop_indices[loop_idx]} / {len(action)} * {scale}') + fraction_complete += msmt.loop_indices[loop_idx] / len(action) * scale + loop_idx += 1 + scale /= len(action) + + return fraction_complete + + def fraction_complete(self, silent=True): + msmt = running_measurement() + if msmt is None: + return 1 + + fraction_complete = 0 + + # Calculate fraction complete from action indices + fraction_complete_actions = self._fraction_complete_action_indices(msmt.action_indices, silent=silent+1) + fraction_complete += fraction_complete_actions + if not silent: + print(f'Fraction complete from action indices: {fraction_complete_actions:.3f}') + + # Calculate fraction complete from point in loop + fraction_complete_loop = self._fraction_complete_loop(msmt.action_indices, silent=silent+1) + fraction_complete += fraction_complete_loop + if not silent: + print(f'Fraction complete from loop: {fraction_complete_loop:.3f}') + + return fraction_complete + # Measurement-related functions def _measure_parameter( self, From 4711f6f86d7573fb3dc6fd47b625e578587c7a86 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 21 Sep 2022 13:52:59 +0200 Subject: [PATCH 084/122] round fraction_complete --- qcodes/dataset/measurement_loop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index b1e7079a02c..e3359778358 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -738,7 +738,7 @@ def _fraction_complete_loop(self, action_indices, silent=True): return fraction_complete - def fraction_complete(self, silent=True): + def fraction_complete(self, silent=True, precision=3): msmt = running_measurement() if msmt is None: return 1 @@ -757,7 +757,7 @@ def fraction_complete(self, silent=True): if not silent: print(f'Fraction complete from loop: {fraction_complete_loop:.3f}') - return fraction_complete + return np.round(fraction_complete, precision) # Measurement-related functions def _measure_parameter( From b2c1997f266de5d3d5c55138e654ffd002c52fb9 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 21 Sep 2022 13:53:14 +0200 Subject: [PATCH 085/122] add test fraction complete --- .../test_measurement_loop_basics.py | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index f85f5f1c31a..193bf35b40b 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -214,9 +214,51 @@ def test_measurement_no_parameter(): assert np.allclose(data_arrays["p1_set"], np.linspace(0, 1, 11)) -# def test_measurement_percentage_complete(): -# with MeasurementLoop("test") as msmt: -# for val in Sweep(np.linspace(0, 1, 11), "p1_set"): -# print(msmt.percentage_complete()) -# msmt.measure(val+1, name="p1_get") -# print(msmt.percentage_complete()) +with MeasurementLoop("test") as msmt: + print(f'Before Sweep') + print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') + for k, val in enumerate(Sweep(np.linspace(0, 1, 10), "p1_set")): + print(f'\n') + print(f'\nBefore first measurement') + print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') + print(f'{msmt.fraction_complete(silent=False)=}') + assert msmt.fraction_complete() == round(0.1*k, 3) + + msmt.measure(val+1, name="p1_get") + print(f'\nBetween first and second measurement') + print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') + print(f'{msmt.fraction_complete(silent=False)=}') + if not k: + assert msmt.fraction_complete() == 0.1 + else: + assert msmt.fraction_complete() == round(0.1*k+0.05, 3) + + msmt.measure(val+1, name="p1_get") + print(f'\nAfter second measurement') + print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') + # print(f'{msmt.fraction_complete(silent=False)=}') + print(f'{msmt.fraction_complete(silent=False)=}') + assert msmt.fraction_complete() == round(0.1 * (k+1), 3) + + for k, val in enumerate(Sweep(np.linspace(0, 1, 10), "p1_set")): + print(f'\n') + print(f'\nBefore first measurement') + print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') + print(f'{msmt.fraction_complete(silent=-1)=}') + assert msmt.fraction_complete() == round(0.5 + 0.05*k, 3) + + msmt.measure(val+1, name="p1_get") + print(f'\nBetween first and second measurement') + print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') + print(f'{msmt.fraction_complete(silent=-1)=}') + if not k: + assert msmt.fraction_complete() == 0.55 + else: + assert msmt.fraction_complete() == round(0.525 + 0.05*k, 3) + + msmt.measure(val+1, name="p1_get") + print(f'\nAfter second measurement') + print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') + # print(f'{msmt.fraction_complete(silent=False)=}') + print(f'{msmt.fraction_complete(silent=-1)=}') + assert msmt.fraction_complete() == round(0.5 + 0.05 * (k+1), 3) \ No newline at end of file From 258c1d4ca7ce8d41213f44d14cd511e9d99a5792 Mon Sep 17 00:00:00 2001 From: Serwan Date: Wed, 21 Sep 2022 15:31:21 +0200 Subject: [PATCH 086/122] Working progress bars --- qcodes/dataset/measurement_loop.py | 56 ++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index e3359778358..f123a006b26 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -4,7 +4,7 @@ from datetime import datetime from time import perf_counter, sleep from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union - +from tqdm.auto import tqdm import numpy as np from qcodes.dataset.data_set_protocol import DataSetProtocol @@ -356,6 +356,8 @@ class MeasurementLoop: name: Measurement name, also used as the dataset name notify: Notify when measurement is complete. The function `Measurement.notify_function` must be set + show_progress: Whether to show progress bars. + If not specified, will use value of class attribute ``MeasurementLoop.show_progress`` """ # Context manager @@ -370,6 +372,10 @@ class MeasurementLoop: except_actions = [] max_arrays = 100 + # Progress bar + show_progress: bool = True + _progress_bar_kwargs: Dict[str, Any] = {'mininterval': 0.2} + _t_start = None # Notification function, called if notify=True. @@ -378,7 +384,7 @@ class MeasurementLoop: # The last three are only not None if an error has occured notify_function = None - def __init__(self, name: Optional[str], notify: bool = False): + def __init__(self, name: Optional[str], notify: bool = False, show_progress: bool = None): self.name: str = name # Data handler is created during `with Measurement("name")` @@ -394,6 +400,12 @@ def __init__(self, name: Optional[str], notify: bool = False): # Index of current action self.action_indices: Union[Tuple[int], None] = None + # Progress bars, only used if show_progress is True + if show_progress is not None: + self.show_progress = show_progress + self.progress_bars: Dict[Tuple[int], tqdm] = {} + + # contains data groups, such as ParameterNodes and nested measurements self._data_groups: Dict[Tuple[int], "MeasurementLoop"] = {} @@ -550,6 +562,9 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb) -> None: # an error occurs during final actions. MeasurementLoop.running_measurement = None + for progress_bar in self.progress_bars.values(): + progress_bar.close() + if exc_type is not None: self.log(f"Measurement error {exc_type.__name__}({exc_val})", level="error") @@ -678,6 +693,33 @@ def _get_maximum_action_index(self, action_indices, position): max_idx = max(max_idx, idxs[position]) return max_idx + def _update_progress_bar(self, action_indices, description=None, create_if_new=True): + # Register new progress bar + if action_indices not in self.progress_bars: + if create_if_new: + self.progress_bars[action_indices] = tqdm( + total=np.prod(self.loop_shape), + desc=description, + **self._progress_bar_kwargs + ) + else: + raise RuntimeError('Cannot update progress bar if not created') + + # Update progress bar + progress_bar = self.progress_bars[action_indices] + value = 1 + for k, loop_idx in enumerate(self.loop_indices[::-1]): + if k: + factor = np.prod(self.loop_shape[-k:]) + else: + factor = 1 + value += factor * loop_idx + + progress_bar.update(value - progress_bar.n) + if value == progress_bar.total: + progress_bar.close() + + def _fraction_complete_action_indices(self, action_indices, silent=True): """Calculate fraction complete from finished action_indices""" msmt = running_measurement() @@ -1032,6 +1074,7 @@ def measure( t0 = perf_counter() initial_action_indices = self.action_indices + # Optionally record timestamp before measurement has been recorded if timestamp: t_now = datetime.now() @@ -1067,6 +1110,15 @@ def measure( f"is not a dict, int, float, bool, or numpy array." ) + # Optionally show progress bar + if self.show_progress: + self._update_progress_bar( + action_indices=initial_action_indices, + description=f'Measuring {self.action_names.get(initial_action_indices)}', + create_if_new=True + ) + + # Optionally record timestamp after measurement has been recorded if timestamp: t_now = datetime.now() From 6e057a8ebb721adfeaddcc652729c0651628cd6c Mon Sep 17 00:00:00 2001 From: Serwan Date: Thu, 22 Sep 2022 08:46:27 +0200 Subject: [PATCH 087/122] Don't show progress bar by default --- qcodes/dataset/measurement_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index f123a006b26..87519d5f7f4 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -373,7 +373,7 @@ class MeasurementLoop: max_arrays = 100 # Progress bar - show_progress: bool = True + show_progress: bool = False _progress_bar_kwargs: Dict[str, Any] = {'mininterval': 0.2} _t_start = None From 85131d42f7e2e0ab3b27ffdb42b9cbe30e5fb978 Mon Sep 17 00:00:00 2001 From: Serwan Date: Thu, 22 Sep 2022 08:46:42 +0200 Subject: [PATCH 088/122] Catch any progress bar errors --- qcodes/dataset/measurement_loop.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 87519d5f7f4..bfed3982149 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -4,6 +4,7 @@ from datetime import datetime from time import perf_counter, sleep from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union +from warnings import warn from tqdm.auto import tqdm import numpy as np @@ -1112,11 +1113,14 @@ def measure( # Optionally show progress bar if self.show_progress: - self._update_progress_bar( - action_indices=initial_action_indices, - description=f'Measuring {self.action_names.get(initial_action_indices)}', - create_if_new=True - ) + try: + self._update_progress_bar( + action_indices=initial_action_indices, + description=f'Measuring {self.action_names.get(initial_action_indices)}', + create_if_new=True + ) + except Exception as e: + warn(f'Failed to update progress bar. Error: {e}') # Optionally record timestamp after measurement has been recorded if timestamp: From 02faed5f3b3bd99c9db95a472c72f69a99aef044 Mon Sep 17 00:00:00 2001 From: Serwan Date: Thu, 22 Sep 2022 08:48:55 +0200 Subject: [PATCH 089/122] add plot functionality to Sweep.execute --- qcodes/dataset/measurement_loop.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index bfed3982149..4d7c5457028 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1679,6 +1679,7 @@ def execute( measure_params: Union[Iterable, _BaseParameter] = None, repetitions: int = 1, sweep: Union[Iterable, "BaseSweep"] = None, + plot: bool = False, ) -> DataSetProtocol: """Performs a measurement using this sweep @@ -1735,6 +1736,9 @@ def execute( with MeasurementLoop(name) as msmt: measure_sweeps(sweeps=sweeps, measure_params=measure_params, msmt=msmt) + if plot and self.plot_function is not None: + self.plot_function(msmt.dataset) + return msmt.dataset # Methods needed to make BaseSweep subclass of AbstractSweep From 248e41be680b938440e9958334702ff006aa8bd9 Mon Sep 17 00:00:00 2001 From: Serwan Date: Thu, 22 Sep 2022 08:51:38 +0200 Subject: [PATCH 090/122] Perform sweep using Sweep(*args)() --- qcodes/dataset/measurement_loop.py | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 4d7c5457028..a7b16e7fe6c 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1634,6 +1634,43 @@ def __next__(self) -> Any: return sweep_value + def __call__( + self, + *args: Optional[Iterable["BaseSweep"]], + name: str = None, + measure_params: Union[Iterable, _BaseParameter] = None, + repetitions: int = 1, + sweep: Union[Iterable, "BaseSweep"] = None, + plot: bool = False, + ): + """Perform sweep, identical to `Sweep.execute` + + + Args: + *args: Optional additional sweeps used for N-dimensional measurements + The first arg is the outermost sweep dimension, and the sweep on which + `Sweep.execute` was called is the innermost dimension. + name: Dataset name, defaults to a concatenation of sweep parameter names + measure_params: Parameters to measure. + If not provided, it will check the attribute ``Station.measure_params`` + for parameters. Raises an error if undefined. + repetitions: Number of times to repeat measurement, defaults to 1. + This will be the outermost loop if set to a value above 1. + sweep: Identical to passing *args. + Note that ``sweep`` can be either a single Sweep, or a Sweep list. + + Returns: + Dataset corresponding to measurement + """ + return self.execute( + *args, + name=name, + measure_params=measure_params, + repetitions=repetitions, + sweep=sweep, + plot=plot + ) + def initialize(self) -> Dict[str, Any]: """Initializes a `Sweep`, attaching it to the current `MeasurementLoop`""" msmt = running_measurement() @@ -1695,6 +1732,9 @@ def execute( This will be the outermost loop if set to a value above 1. sweep: Identical to passing *args. Note that ``sweep`` can be either a single Sweep, or a Sweep list. + + Returns: + Dataset corresponding to measurement """ # Get "measure_params" from station if not provided if measure_params is None: From 8385b9c3ea2b838565b2c9ad407a6fa5d27e23ad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 Sep 2022 13:07:40 +0000 Subject: [PATCH 091/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 15 ++++++++------- .../test_measurement_loop_basics.py | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index a7b16e7fe6c..adf05ff0719 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -5,8 +5,9 @@ from time import perf_counter, sleep from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union from warnings import warn -from tqdm.auto import tqdm + import numpy as np +from tqdm.auto import tqdm from qcodes.dataset.data_set_protocol import DataSetProtocol from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement @@ -754,14 +755,14 @@ def _fraction_complete_loop(self, action_indices, silent=True): fraction_complete = 0 scale = 1 loop_idx = 0 - + for k, action_idx in enumerate(action_indices): # Check if current action is a sweep # If so, reduce scale by action index fraction action = msmt.actions.get(action_indices[:k+1]) if isinstance(action, BaseSweep): max_idx = self._get_maximum_action_index(action_indices, position=k) - + if not silent: print(f'Reducing current Sweep {loop_idx=} {msmt.loop_indices[loop_idx]} / {len(action)} * {scale}') print(f'{max_idx=}') @@ -1115,7 +1116,7 @@ def measure( if self.show_progress: try: self._update_progress_bar( - action_indices=initial_action_indices, + action_indices=initial_action_indices, description=f'Measuring {self.action_names.get(initial_action_indices)}', create_if_new=True ) @@ -1644,7 +1645,7 @@ def __call__( plot: bool = False, ): """Perform sweep, identical to `Sweep.execute` - + Args: *args: Optional additional sweeps used for N-dimensional measurements @@ -1663,8 +1664,8 @@ def __call__( Dataset corresponding to measurement """ return self.execute( - *args, - name=name, + *args, + name=name, measure_params=measure_params, repetitions=repetitions, sweep=sweep, diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 193bf35b40b..b25866360da 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -261,4 +261,4 @@ def test_measurement_no_parameter(): print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') # print(f'{msmt.fraction_complete(silent=False)=}') print(f'{msmt.fraction_complete(silent=-1)=}') - assert msmt.fraction_complete() == round(0.5 + 0.05 * (k+1), 3) \ No newline at end of file + assert msmt.fraction_complete() == round(0.5 + 0.05 * (k+1), 3) From 9d83dab94e8c00488b6fca7153ccecfc410a8580 Mon Sep 17 00:00:00 2001 From: Serwan Date: Fri, 23 Sep 2022 15:28:21 +0200 Subject: [PATCH 092/122] override parameter.sweep --- qcodes/parameters/parameter.py | 92 ++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/qcodes/parameters/parameter.py b/qcodes/parameters/parameter.py index b52c1e3b331..6df6ceb3e3f 100644 --- a/qcodes/parameters/parameter.py +++ b/qcodes/parameters/parameter.py @@ -390,6 +390,98 @@ def sweep( return SweepFixedValues(self, start=start, stop=stop, step=step, num=num) + def sweep( + self, + *args, + start: float = None, + stop: float = None, + around: float = None, + num: int = None, + step: float = None, + delay: float = None, + initial_delay: float = None, + revert: bool = None, + measurement_name: str = None, + measure_params: ParameterBase=None, + repetitions: int = 1, + sweep=None, + plot: bool = False, + ): + """Perform a measurement by sweeping this parameter + + This creates a `Sweep` object and executes a measurement with it. + + For the most frequent use-cases, a Sweep can also be created by passing args in a variety of ways: + + 1 arg: + - parameter.sweep([1,2,3]) + : sweep "parameter" over sequence [1,2,3] + - parameter.sweep(stop_val) + : sweep "parameter" from current value to "stop_val" + 2 args: + - parameter.sweep(start_val, stop_val) + : sweep "parameter" from "start_val" to "stop_val" + If "num" or "step" is not given as kwarg, it will check if "num" or "step" + is set in dict "parameter.sweep_defaults" and use that, or else raise an error. + 3 args: + - Sweep(start_val, stop_val, num) + : Sweep "parameter" from "start_val" to "stop_val" with "num" number of points + + Args: + start: start value of sweep sequence + Cannot be used together with ``around`` + stop: stop value of sweep sequence + Cannot be used together with ``around`` + around: sweep around the current parameter value. + ``start`` and ``stop`` are defined from ``around`` and the current value + i.e. start=X-dx, stop=X+dx when current_value=X and around=dx. + Passing the kwarg "around" also sets revert=True unless explicitly set False + num: Number of points between start and stop. + Cannot be used together with ``step`` + step: Increment from start to stop. + Cannot be used together with ``num`` + delay: Time delay after incrementing to the next value + initial_delay: Time delay after having incremented to its first value + name: Sweep name, overrides parameter.name + label: Sweep label, overrides parameter.label + unit: Sweep unit, overrides parameter.unit + revert: Revert parameter back to original value after the sweep ends. + This is False by default, unless the kwarg ``around`` is passed + measurement_name: Dataset name, defaults to a concatenation of sweep parameter names + measure_params: Parameters to measure. + If not provided, it will check the attribute ``Station.measure_params`` + for parameters. Raises an error if undefined. + repetitions: Number of times to repeat measurement, defaults to 1. + This will be the outermost loop if set to a value above 1. + sweep: Additional sweeps used for N-dimensional measurements + The first element is the outermost sweep dimension, and the sweep on which + `parameter.sweep` was called is the innermost dimension. + Note that ``sweep`` can be either a single Sweep, or a Sweep list. + """ + from qcodes.dataset import Sweep + sweep = Sweep( + self, # Pass parameter as first arg + *args, + start=start, + stop=stop, + around=around, + num=num, + step=step, + delay=delay, + initial_delay=initial_delay, + revert=revert + ) + + dataset = sweep.execute( + name=measurement_name, + measure_params=measure_params, + repetitions=repetitions, + sweep=sweep, + plot=plot, + ) + return dataset + + class ManualParameter(Parameter): def __init__( self, From 15029a8ecf07bb59123d17d2b08db7bb02a4c712 Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Mon, 17 Oct 2022 15:59:25 +0200 Subject: [PATCH 093/122] mistakenly added multiple sweeps --- qcodes/dataset/measurement_loop.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index adf05ff0719..bae581c4009 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -434,7 +434,10 @@ def __init__(self, name: Optional[str], notify: bool = False, show_progress: boo @property def dataset(self) -> DataSetProtocol: - return self.data_handler.dataset + if self.data_handler is None: + return None + else: + return self.data_handler.dataset def log(self, message: str, level: str = "info") -> None: """Send a log message @@ -1753,12 +1756,13 @@ def execute( # Create list of sweeps sweeps = list(args) - if not all(isinstance(sweep, BaseSweep) for sweep in sweeps): - raise ValueError("Args passed to Sweep.execute must be Sweeps") if isinstance(sweep, BaseSweep): sweeps.append(sweep) elif isinstance(sweep, (list, tuple)): - sweeps += list(sweep) + sweeps.extend(sweep) + + if not all(isinstance(sweep, BaseSweep) for sweep in sweeps): + raise ValueError("Args passed to Sweep.execute must be Sweeps") # Add repetition as a sweep if > 1 if repetitions > 1: From 611e7a75c6dfd591b1590fd669324c457917fd17 Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Mon, 17 Oct 2022 16:02:07 +0200 Subject: [PATCH 094/122] fix in parameter sweep --- qcodes/parameters/parameter.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qcodes/parameters/parameter.py b/qcodes/parameters/parameter.py index 6df6ceb3e3f..d0400be35ff 100644 --- a/qcodes/parameters/parameter.py +++ b/qcodes/parameters/parameter.py @@ -459,7 +459,7 @@ def sweep( Note that ``sweep`` can be either a single Sweep, or a Sweep list. """ from qcodes.dataset import Sweep - sweep = Sweep( + parameter_sweep = Sweep( self, # Pass parameter as first arg *args, start=start, @@ -471,8 +471,7 @@ def sweep( initial_delay=initial_delay, revert=revert ) - - dataset = sweep.execute( + dataset = parameter_sweep.execute( name=measurement_name, measure_params=measure_params, repetitions=repetitions, From 2106278846a4803e9c58118c63387cc206bf8535 Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Tue, 18 Oct 2022 13:55:05 +0200 Subject: [PATCH 095/122] Add Exception to except --- qcodes/instrument/instrument_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/instrument/instrument_base.py b/qcodes/instrument/instrument_base.py index 67d04576083..ba5c17e1a72 100644 --- a/qcodes/instrument/instrument_base.py +++ b/qcodes/instrument/instrument_base.py @@ -285,7 +285,7 @@ def snapshot_base( update_par = update try: snap["parameters"][name] = param.snapshot(update=update_par) - except: + except Exception: # really log this twice. Once verbose for the UI and once # at lower level with more info for file based loggers self.log.warning("Snapshot: Could not update parameter: %s", name) From 83569371e066b88279618d3b94596443fe333ea4 Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Tue, 18 Oct 2022 16:28:30 +0200 Subject: [PATCH 096/122] Convert None to NaN so it doesn't raise an error --- qcodes/dataset/measurement_loop.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index bae581c4009..7e6c5511adc 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -838,6 +838,10 @@ def _measure_parameter( # Get parameter result result = parameter(**kwargs) + # Result "None causes issues, so it's converted to NaN" + if result is None: + result = np.nan + self.data_handler.add_measurement_result( action_indices=self.action_indices, result=result, From cf7af509df5857338a17ea78d5c3eae930cb63f8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 18 Oct 2022 14:28:58 +0000 Subject: [PATCH 097/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/utils/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index c78e6235d83..6c070c7bbe1 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -150,7 +150,7 @@ def record(self, key: str, val: Any = None) -> None: for _ in range(len(timing_list) - self.max_records): timing_list.pop(0) - + @deprecate("Internal function no longer part of the public qcodes api") def compare_dictionaries( dict_1: Dict[Hashable, Any], From ecea8392c28c29f0fde9cb5cfed7eb2e1ac6a332 Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Tue, 18 Oct 2022 16:48:34 +0200 Subject: [PATCH 098/122] minor fix to masking/unmasking --- qcodes/dataset/measurement_loop.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 7e6c5511adc..abe3ad81dfb 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1332,6 +1332,14 @@ def unmask( # A masked property has been passed, which we unmask here try: original_value = kwargs["original_value"] + if unmask_type is None: + if isinstance(obj, Parameter): + unmask_type = "parameter" + elif isinstance(obj, dict): + unmask_type = "key" + elif hasattr(obj, attr): + unmask_type = "attr" + if unmask_type == "key": obj[key] = original_value elif unmask_type == "attr": @@ -1574,6 +1582,8 @@ def __iter__(self) -> Iterable: if self.revert: if isinstance(self.sequence, SweepValues): msmt.mask(self.sequence.parameter, self.sequence.parameter.get()) + elif self.parameter is not None: + msmt.mask(self.parameter, self.parameter.get()) else: raise NotImplementedError("Unable to revert non-parameter values.") From 67d969c598c1e7aa692f8952400e07ad0567ae54 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 18 Oct 2022 14:50:42 +0000 Subject: [PATCH 099/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index abe3ad81dfb..4001cf33145 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1339,7 +1339,7 @@ def unmask( unmask_type = "key" elif hasattr(obj, attr): unmask_type = "attr" - + if unmask_type == "key": obj[key] = original_value elif unmask_type == "attr": From c9400b1f2eb3e6fb132d96693ea17997fd6ff9c1 Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Tue, 18 Oct 2022 18:04:21 +0200 Subject: [PATCH 100/122] reverting tqdm --- qcodes/dataset/measurement_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index abe3ad81dfb..6cc290fe0c1 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -7,7 +7,7 @@ from warnings import warn import numpy as np -from tqdm.auto import tqdm +from tqdm import tqdm from qcodes.dataset.data_set_protocol import DataSetProtocol from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement From 3ced8d89752efeee955de313be8346bc25bab5d0 Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Tue, 18 Oct 2022 18:04:59 +0200 Subject: [PATCH 101/122] Fix plot function --- qcodes/dataset/measurement_loop.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 4001cf33145..70a74bb480b 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1509,6 +1509,7 @@ class BaseSweep(AbstractSweep): for param_val in Sweep(p. ``` """ + plot_function = None def __init__( self, @@ -1795,8 +1796,8 @@ def execute( with MeasurementLoop(name) as msmt: measure_sweeps(sweeps=sweeps, measure_params=measure_params, msmt=msmt) - if plot and self.plot_function is not None: - self.plot_function(msmt.dataset) + if plot and Sweep.plot_function is not None and MeasurementLoop.running_measurement is None: + Sweep.plot_function(msmt.dataset) return msmt.dataset From 147b983ec9e88ffc0df1d9c35caecb28f879189a Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Tue, 18 Oct 2022 18:05:40 +0200 Subject: [PATCH 102/122] better plotting logic --- qcodes/parameters/parameter.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/qcodes/parameters/parameter.py b/qcodes/parameters/parameter.py index abbc2b873b4..cdb75f88461 100644 --- a/qcodes/parameters/parameter.py +++ b/qcodes/parameters/parameter.py @@ -415,7 +415,7 @@ def sweep( measure_params: ParameterBase=None, repetitions: int = 1, sweep=None, - plot: bool = False, + plot: bool = None, ): """Perform a measurement by sweeping this parameter @@ -468,7 +468,7 @@ def sweep( `parameter.sweep` was called is the innermost dimension. Note that ``sweep`` can be either a single Sweep, or a Sweep list. """ - from qcodes.dataset import Sweep + from qcodes.dataset import MeasurementLoop, Sweep parameter_sweep = Sweep( self, # Pass parameter as first arg *args, @@ -481,6 +481,12 @@ def sweep( initial_delay=initial_delay, revert=revert ) + + # Only plot if not excplicitly set and not part of a larger measurement + if plot is None: + plot = (MeasurementLoop.running_measurement is None) + + dataset = parameter_sweep.execute( name=measurement_name, measure_params=measure_params, From 1dca9e8c6509aaeb83125940cd7e7163479abd0d Mon Sep 17 00:00:00 2001 From: Serwan Date: Fri, 28 Oct 2022 16:28:16 +0200 Subject: [PATCH 103/122] show plot after sweep --- qcodes/dataset/measurement_loop.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 70a74bb480b..143c69522f7 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -5,6 +5,7 @@ from time import perf_counter, sleep from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union from warnings import warn +from matplotlib import pyplot as plt import numpy as np from tqdm.auto import tqdm @@ -1798,6 +1799,7 @@ def execute( if plot and Sweep.plot_function is not None and MeasurementLoop.running_measurement is None: Sweep.plot_function(msmt.dataset) + plt.show() return msmt.dataset From ad7db8d5807033e06074d55fd4d37962a2c5eed7 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 8 Nov 2022 14:56:06 +0100 Subject: [PATCH 104/122] remove sweep stop arg it can cause mistakes --- qcodes/dataset/measurement_loop.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 70a74bb480b..55c1c383ff6 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1846,8 +1846,6 @@ class Sweep(BaseSweep): 2 args: - Sweep(parameter, [1,2,3]) : sweep "parameter" over sequence [1,2,3] - - Sweep(parameter, stop_val) - : sweep "parameter" from current value to "stop_val" - Sweep([1,2,3], "name") : sweep over sequence [1,2,3] with sweep array name "name" 3 args: @@ -1951,8 +1949,6 @@ def _transform_args_to_kwargs(self, *args, **kwargs) -> Tuple[dict]: 2 args: - Sweep(parameter, [1,2,3]) : sweep "parameter" over sequence [1,2,3] - - Sweep(parameter, stop_val) - : sweep "parameter" from current value to "stop_val" - Sweep([1,2,3], "name") : sweep over sequence [1,2,3] with sweep array name "name" 3 args: @@ -1989,12 +1985,10 @@ def _transform_args_to_kwargs(self, *args, **kwargs) -> Tuple[dict]: if isinstance(args[0], _BaseParameter): # Sweep(parameter, [1,2,3]) if isinstance(args[1], Iterable): kwargs["parameter"], kwargs["sequence"] = args - elif isinstance(args[1], (int, float)): - kwargs["parameter"], kwargs["stop"] = args else: raise SyntaxError( "Sweep with Parameter arg and second arg should have second arg" - " be either a sequence or a target value" + " be a sequence" ) elif isinstance(args[0], Iterable): # Sweep([1,2,3], "name") assert isinstance(args[1], str) From 3eecb6c456c02c50e59dcddce1207655e3359ab2 Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Fri, 11 Nov 2022 09:33:39 +0100 Subject: [PATCH 105/122] ignore num when step is given --- qcodes/dataset/measurement_loop.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 55c1c383ff6..05252984ab8 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -2025,6 +2025,8 @@ def _transform_args_to_kwargs(self, *args, **kwargs) -> Tuple[dict]: # Update kwargs with sweep_defaults from parameter if hasattr(kwargs["parameter"], "sweep_defaults"): for key, val in kwargs["parameter"].sweep_defaults.items(): + if key == 'num' and 'step' in kwargs: + continue if kwargs.get(key) is None: kwargs[key] = val From dbcc85042a03e477519f1b23e35df50e7e1288c0 Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Sun, 13 Nov 2022 16:54:02 +0100 Subject: [PATCH 106/122] fix step + num --- qcodes/dataset/measurement_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 05252984ab8..cac02294473 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -2025,7 +2025,7 @@ def _transform_args_to_kwargs(self, *args, **kwargs) -> Tuple[dict]: # Update kwargs with sweep_defaults from parameter if hasattr(kwargs["parameter"], "sweep_defaults"): for key, val in kwargs["parameter"].sweep_defaults.items(): - if key == 'num' and 'step' in kwargs: + if key == 'num' and kwargs.get('step') is not None: continue if kwargs.get(key) is None: kwargs[key] = val From bf61df7e07f46ef1c80921119d20c845fa65618c Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 15 Nov 2022 15:02:40 +0100 Subject: [PATCH 107/122] include metadata --- qcodes/dataset/measurement_loop.py | 87 ++++++++++++++---------------- qcodes/utils/helpers.py | 2 +- 2 files changed, 40 insertions(+), 49 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index cac02294473..286326033da 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1,3 +1,5 @@ +import builtins +import json import logging import threading import traceback @@ -29,6 +31,7 @@ from qcodes.station import Station from qcodes.utils.dataset.doNd import AbstractSweep, ActionsT from qcodes.utils.helpers import PerformanceTimer +from qcodes.utils import NumpyJSONEncoder RAW_VALUE_TYPES = ( float, @@ -502,13 +505,9 @@ def __enter__(self) -> "MeasurementLoop": measurement_loop=self, name=self.name ) - # TODO incorporate metadata - # self._initialize_metadata(self.dataset) - # with self.timings.record(["dataset", "save_metadata"]): - # self.dataset.save_metadata() - - # if hasattr(self.dataset, "save_config"): - # self.dataset.save_config() + # Add metadata + self._t_start = datetime.now() + self._initialize_metadata(self.dataset) # Initialize attributes self.loop_shape = () @@ -517,8 +516,6 @@ def __enter__(self) -> "MeasurementLoop": self.data_arrays = {} self.set_arrays = {} - # self.log(f"Measurement started {self.dataset.location}") - else: if threading.current_thread() is not MeasurementLoop.measurement_thread: raise RuntimeError( @@ -596,10 +593,13 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb) -> None: except Exception: self.log("Could not notify", level="error") - # TODO include metadata - # t_stop = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - # self.data_handler.add_metadata({"t_stop": t_stop}) - # self.data_handler.add_metadata({"timings": self.timings}) + # include final metadata + t_stop = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.dataset.add_metadata("t_stop", t_stop) + self.dataset.add_metadata( + "timings", + json.dumps(dict(self.timings.timings), cls=NumpyJSONEncoder) + ) self.data_handler.finalize() self.log("Measurement finished") @@ -607,41 +607,32 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb) -> None: msmt.step_out(reduce_dimension=False) self.is_context_manager = False - self._t_start = datetime.now() - - # TODO Needs to be implemented - # def _initialize_metadata(self, dataset): - # """Initialize dataset metadata""" - # if dataset is None: - # dataset = self.dataset - - # config = qcodes_config - # dataset.add_metadata({"config": config}) - - # dataset.add_metadata({"measurement_type": "Measurement"}) - - # # Add instrument information - # if Station.default is not None: - # dataset.add_metadata({"station": Station.default.snapshot()}) - - # if using_ipython(): - # measurement_cell = get_last_input_cells(1)[0] - - # measurement_code = measurement_cell - # # If the code is run from a measurement thread, there is some - # # initial code that should be stripped - # init_string = "get_ipython().run_cell_magic("new_job", ", " - # if measurement_code.startswith(init_string): - # measurement_code = measurement_code[len(init_string) + 1 : -4] - - # dataset.add_metadata( - # { - # "measurement_cell": measurement_cell, - # "measurement_code": measurement_code, - # "last_input_cells": get_last_input_cells(20), - # "t_start": self._t_start.strftime("%Y-%m-%d %H:%M:%S") - # } - # ) + + def _initialize_metadata(self, dataset): + """Initialize dataset metadata""" + if dataset is None: + dataset = self.dataset + + # Save config to metadata + try: + from qcodes import config + + config_str = json.dumps(dict(config), cls=NumpyJSONEncoder) + self.dataset.add_metadata('config', config_str) + except Exception as e: + warn(f'Could not save config due to error {e}') + + dataset.add_metadata("measurement_type", "MeasurementLoop") + dataset.add_metadata("t_start", self._t_start.strftime("%Y-%m-%d %H:%M:%S")) + + # Save latest IPython cells + from IPython import get_ipython + shell = get_ipython() + if shell is not None and "In" in shell.ns_table["user_global"]: + num_cells = 20 # Number of cells to save + last_input_cells = shell.ns_table["user_global"]['In'][-num_cells:] + dataset.add_metadata("measurement_code", last_input_cells[-1]) + dataset.add_metadata("last_input_cells", str(last_input_cells)) def _verify_action( self, action: Callable, name: str, add_if_new: bool = True diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index 6c070c7bbe1..23d07f2195d 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -47,7 +47,7 @@ def warn_units(class_name: str, instance: object) -> None: import builtins import sys import time -from pprint import pprint +import pprint import numpy as np From 0ad7dadad68ba1dbe27b349404167128a7082c8d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Nov 2022 14:03:04 +0000 Subject: [PATCH 108/122] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- qcodes/dataset/measurement_loop.py | 2 +- qcodes/utils/helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 286326033da..3f46809b8a8 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -29,9 +29,9 @@ from qcodes.instrument.parameter import _BaseParameter from qcodes.parameters import ParameterBase from qcodes.station import Station +from qcodes.utils import NumpyJSONEncoder from qcodes.utils.dataset.doNd import AbstractSweep, ActionsT from qcodes.utils.helpers import PerformanceTimer -from qcodes.utils import NumpyJSONEncoder RAW_VALUE_TYPES = ( float, diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index 23d07f2195d..57cb813eca8 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -45,9 +45,9 @@ def warn_units(class_name: str, instance: object) -> None: # TODO these functions need a place import builtins +import pprint import sys import time -import pprint import numpy as np From a29ff55458da9bcd200418fad27b50e2125148a7 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 6 Dec 2022 11:15:38 +0100 Subject: [PATCH 109/122] begun adding measurement loop array --- qcodes/dataset/measurement_loop.py | 88 +++++++++++++++++-- .../test_measurement_loop_basics.py | 5 ++ 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index da782570763..514a711e6e8 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -35,7 +35,6 @@ float, int, bool, - np.ndarray, np.integer, np.floating, np.bool_, @@ -222,6 +221,7 @@ def add_measurement_result( name: Optional[str] = None, label: Optional[str] = None, unit: Optional[str] = None, + setpoints: Optional[Iterable] = None, ) -> None: """Store single measurement result @@ -255,10 +255,11 @@ def add_measurement_result( ) # Store result - setpoints = [ - self.setpoint_list[action_indices]["latest_value"] - for action_indices in measurement_info["setpoints_action_indices"] - ] + if setpoints is None: + setpoints = [ + self.setpoint_list[action_indices]["latest_value"] + for action_indices in measurement_info["setpoints_action_indices"] + ] parameters = ( *measurement_info["setpoint_parameters"], measurement_info["dataset_parameter"], @@ -269,6 +270,41 @@ def add_measurement_result( # Also store in measurement_info measurement_info["latest_value"] = result + def add_measurement_result_array( + self, + action_indices: Tuple[int], + result: Union[float, int, bool], + parameter: _BaseParameter = None, + name: Optional[str] = None, + label: Optional[str] = None, + unit: Optional[str] = None, + ) -> None: + assert np.ndim(result) == 1, "Currently only able to handle 1D array" + + # Determine setpoints + setpoint_info = self.setpoint_list[action_indices] + setpoints = [] + for k, sweep in enumerate(setpoint_info['setpoint_parameters']): + if k < len(setpoint_info['setpoint_parameters']) - 1: + # Inner setpoints, repeat value by length of array + latest_value = self.setpoint_list[action_indices]["latest_value"] + sweep_arr = np.repeat(latest_value, len(result)) + else: + # Innermost loop, get sequence + sweep_arr = sweep.sequence + assert len(sweep_arr) == len(result) + setpoints.append(sweep_arr) + + self.add_measurement_result( + action_indices=action_indices, + result=result, + parameter=parameter, + name=name, + label=label, + unit=unit, + setpoints=setpoints + ) + def _update_interdependencies(self) -> None: """Updates dataset after instantiation to include new setpoint/measurement parameter @@ -947,6 +983,41 @@ def _measure_callable( return results + def _measure_array( + self, + array: Union[list, np.ndarray], + name: str, + label: str = None, + unit: str = None, + setpoints: 'Sweep' = None + ): + # Ensure setpoints is a Sweep + if setpoints is None: + setpoints_sequence = range(len(array)) + setpoints = Sweep(setpoints_sequence, name='setpoint_idx', label='Setpoint index') + elif isinstance(setpoints, (list, np.ndarray)): + # Convert sequence to Sweep + setpoints = Sweep(setpoints, name='setpoint_idx', label='Setpoint index') + elif not isinstance(setpoints, Sweep): + raise SyntaxError('Cannot measure because array setpoints not understood') + + # Enter sweep + iter(setpoints) + + # Ensure measuring array matches the current action_indices + self._verify_action(action=None, name=name, add_if_new=True) + + self.data_handler.add_measurement_result_array( + action_indices=self.action_indices, + result=array, + parameter=None, + name=name, + label=label, + unit=unit, + ) + + setpoints.exit_sweep() + def _measure_dict(self, value: dict, name: str) -> Dict[str, Any]: """Store dictionary results @@ -1007,16 +1078,15 @@ def _measure_value( elif isinstance(value, (bool, np.bool_)): value = int(value) - result = value self.data_handler.add_measurement_result( action_indices=self.action_indices, - result=result, + result=value, parameter=parameter, name=name, label=label, unit=unit, ) - return result + return value def measure( self, @@ -1109,6 +1179,8 @@ def measure( result = self._measure_callable(measurable, name=name, **kwargs) elif isinstance(measurable, dict): result = self._measure_dict(measurable, name=name) + elif isinstance(measurable, (list, np.ndarray)): + result = self._measure_array(measurable, name=name, setpoints=kwargs.get("setpoints")) elif isinstance(measurable, RAW_VALUE_TYPES): result = self._measure_value( measurable, name=name, label=label, unit=unit, **kwargs diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index b25866360da..48ccf799bbd 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -262,3 +262,8 @@ def test_measurement_no_parameter(): # print(f'{msmt.fraction_complete(silent=False)=}') print(f'{msmt.fraction_complete(silent=-1)=}') assert msmt.fraction_complete() == round(0.5 + 0.05 * (k+1), 3) + + +def test_save_array_0D(): + with MeasurementLoop('array_0D') as msmt: + msmt.measure([1,2,3], 'array') \ No newline at end of file From 391b5d582bcda1f16bf5c48f60b51afbe3f005c3 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 6 Dec 2022 11:27:33 +0100 Subject: [PATCH 110/122] fixed mistaken test --- .../test_measurement_loop_basics.py | 95 ++++++++++--------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 48ccf799bbd..40b1349e8ad 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -214,54 +214,55 @@ def test_measurement_no_parameter(): assert np.allclose(data_arrays["p1_set"], np.linspace(0, 1, 11)) -with MeasurementLoop("test") as msmt: - print(f'Before Sweep') - print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') - for k, val in enumerate(Sweep(np.linspace(0, 1, 10), "p1_set")): - print(f'\n') - print(f'\nBefore first measurement') - print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') - print(f'{msmt.fraction_complete(silent=False)=}') - assert msmt.fraction_complete() == round(0.1*k, 3) - - msmt.measure(val+1, name="p1_get") - print(f'\nBetween first and second measurement') - print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') - print(f'{msmt.fraction_complete(silent=False)=}') - if not k: - assert msmt.fraction_complete() == 0.1 - else: - assert msmt.fraction_complete() == round(0.1*k+0.05, 3) - - msmt.measure(val+1, name="p1_get") - print(f'\nAfter second measurement') - print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') - # print(f'{msmt.fraction_complete(silent=False)=}') - print(f'{msmt.fraction_complete(silent=False)=}') - assert msmt.fraction_complete() == round(0.1 * (k+1), 3) - - for k, val in enumerate(Sweep(np.linspace(0, 1, 10), "p1_set")): - print(f'\n') - print(f'\nBefore first measurement') - print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') - print(f'{msmt.fraction_complete(silent=-1)=}') - assert msmt.fraction_complete() == round(0.5 + 0.05*k, 3) - - msmt.measure(val+1, name="p1_get") - print(f'\nBetween first and second measurement') - print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') - print(f'{msmt.fraction_complete(silent=-1)=}') - if not k: - assert msmt.fraction_complete() == 0.55 - else: - assert msmt.fraction_complete() == round(0.525 + 0.05*k, 3) - - msmt.measure(val+1, name="p1_get") - print(f'\nAfter second measurement') +def test_measurement_fraction_complete(): + with MeasurementLoop("test") as msmt: + print(f'Before Sweep') print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') - # print(f'{msmt.fraction_complete(silent=False)=}') - print(f'{msmt.fraction_complete(silent=-1)=}') - assert msmt.fraction_complete() == round(0.5 + 0.05 * (k+1), 3) + for k, val in enumerate(Sweep(np.linspace(0, 1, 10), "p1_set")): + print(f'\n') + print(f'\nBefore first measurement') + print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') + print(f'{msmt.fraction_complete(silent=False)=}') + assert msmt.fraction_complete() == round(0.1*k, 3) + + msmt.measure(val+1, name="p1_get") + print(f'\nBetween first and second measurement') + print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') + print(f'{msmt.fraction_complete(silent=False)=}') + if not k: + assert msmt.fraction_complete() == 0.1 + else: + assert msmt.fraction_complete() == round(0.1*k+0.05, 3) + + msmt.measure(val+1, name="p1_get") + print(f'\nAfter second measurement') + print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') + # print(f'{msmt.fraction_complete(silent=False)=}') + print(f'{msmt.fraction_complete(silent=False)=}') + assert msmt.fraction_complete() == round(0.1 * (k+1), 3) + + for k, val in enumerate(Sweep(np.linspace(0, 1, 10), "p1_set")): + print(f'\n') + print(f'\nBefore first measurement') + print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') + print(f'{msmt.fraction_complete(silent=-1)=}') + assert msmt.fraction_complete() == round(0.5 + 0.05*k, 3) + + msmt.measure(val+1, name="p1_get") + print(f'\nBetween first and second measurement') + print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') + print(f'{msmt.fraction_complete(silent=-1)=}') + if not k: + assert msmt.fraction_complete() == 0.55 + else: + assert msmt.fraction_complete() == round(0.525 + 0.05*k, 3) + + msmt.measure(val+1, name="p1_get") + print(f'\nAfter second measurement') + print(f'{msmt.action_indices=}, {msmt.loop_indices=}, {msmt.loop_shape=}') + # print(f'{msmt.fraction_complete(silent=False)=}') + print(f'{msmt.fraction_complete(silent=-1)=}') + assert msmt.fraction_complete() == round(0.5 + 0.05 * (k+1), 3) def test_save_array_0D(): From a7a45a9b887e49cedd5fac904a14e53da1d48fa4 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 6 Dec 2022 12:03:56 +0100 Subject: [PATCH 111/122] ran through first test without errors --- qcodes/dataset/measurement_loop.py | 75 ++++++++----------- .../test_measurement_loop_basics.py | 5 +- 2 files changed, 36 insertions(+), 44 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 514a711e6e8..884d9552838 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -221,7 +221,6 @@ def add_measurement_result( name: Optional[str] = None, label: Optional[str] = None, unit: Optional[str] = None, - setpoints: Optional[Iterable] = None, ) -> None: """Store single measurement result @@ -254,12 +253,10 @@ def add_measurement_result( f"{measurement_info['parameter'].name}" ) - # Store result - if setpoints is None: - setpoints = [ - self.setpoint_list[action_indices]["latest_value"] - for action_indices in measurement_info["setpoints_action_indices"] - ] + # Get setpoints corresponding to measurement + setpoints = self.get_result_setpoints(result, action_indices=action_indices) + + # Store results parameters = ( *measurement_info["setpoint_parameters"], measurement_info["dataset_parameter"], @@ -270,40 +267,31 @@ def add_measurement_result( # Also store in measurement_info measurement_info["latest_value"] = result - def add_measurement_result_array( - self, - action_indices: Tuple[int], - result: Union[float, int, bool], - parameter: _BaseParameter = None, - name: Optional[str] = None, - label: Optional[str] = None, - unit: Optional[str] = None, - ) -> None: - assert np.ndim(result) == 1, "Currently only able to handle 1D array" - - # Determine setpoints - setpoint_info = self.setpoint_list[action_indices] - setpoints = [] - for k, sweep in enumerate(setpoint_info['setpoint_parameters']): - if k < len(setpoint_info['setpoint_parameters']) - 1: - # Inner setpoints, repeat value by length of array - latest_value = self.setpoint_list[action_indices]["latest_value"] - sweep_arr = np.repeat(latest_value, len(result)) - else: - # Innermost loop, get sequence - sweep_arr = sweep.sequence - assert len(sweep_arr) == len(result) - setpoints.append(sweep_arr) + def get_result_setpoints(self, result, action_indices): + # Check if result is an array + if np.ndim(result) > 0: + measurement_info = self.measurement_list[action_indices] + setpoints = [] + + for k, setpoint_indices in enumerate(measurement_info["setpoints_action_indices"]): + setpoint_info = self.setpoint_list[setpoint_indices] + if k < len(measurement_info["setpoints_action_indices"]) - 1: + # Inner setpoints, repeat value by length of array + latest_value = setpoint_info["latest_value"] + sweep_arr = np.repeat(latest_value, len(result)) + else: + # Innermost loop, get sequence + sweep_arr = setpoint_info["sweep"].sequence + assert len(sweep_arr) == len(result) - self.add_measurement_result( - action_indices=action_indices, - result=result, - parameter=parameter, - name=name, - label=label, - unit=unit, - setpoints=setpoints - ) + setpoints.append(sweep_arr) + else: + setpoints = [ + self.setpoint_list[action_indices]["latest_value"] + for action_indices in measurement_info["setpoints_action_indices"] + ] + + return setpoints def _update_interdependencies(self) -> None: """Updates dataset after instantiation to include new setpoint/measurement parameter @@ -428,7 +416,7 @@ def __init__(self, name: Optional[str], notify: bool = False, show_progress: boo # Data handler is created during `with Measurement("name")` # Used to control dataset(s) - self.data_handler: DataSaver = None + self.data_handler: _DatasetHandler = None # Total dimensionality of loop self.loop_shape: Union[Tuple[int], None] = None @@ -1007,7 +995,7 @@ def _measure_array( # Ensure measuring array matches the current action_indices self._verify_action(action=None, name=name, add_if_new=True) - self.data_handler.add_measurement_result_array( + self.data_handler.add_measurement_result( action_indices=self.action_indices, result=array, parameter=None, @@ -1709,6 +1697,7 @@ def __next__(self) -> Any: # TODO: Check what other iterators might be able to be masked pass self.exit_sweep() + raise StopIteration # Set parameter if passed along if self.parameter is not None and self.parameter.settable: @@ -1781,6 +1770,7 @@ def initialize(self) -> Dict[str, Any]: ) setpoint_info = { + "sweep": self, "parameter": self.parameter, "latest_value": None, "registered": False, @@ -1799,7 +1789,6 @@ def exit_sweep(self) -> None: """Exits sweep, stepping out of the current `Measurement.action_indices`""" msmt = running_measurement() msmt.step_out(reduce_dimension=True) - raise StopIteration def execute( self, diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 40b1349e8ad..8ab6cd5f9a5 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -265,6 +265,9 @@ def test_measurement_fraction_complete(): assert msmt.fraction_complete() == round(0.5 + 0.05 * (k+1), 3) + def test_save_array_0D(): with MeasurementLoop('array_0D') as msmt: - msmt.measure([1,2,3], 'array') \ No newline at end of file + msmt.measure([1,2,3], 'array') + + \ No newline at end of file From a9020ef71163d1ed81f50134ef85304d699ebe30 Mon Sep 17 00:00:00 2001 From: Serwan Date: Tue, 6 Dec 2022 12:20:03 +0100 Subject: [PATCH 112/122] measuring arrays works --- qcodes/dataset/measurement_loop.py | 4 +- .../test_measurement_loop_basics.py | 81 ++++++++++++++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 884d9552838..4f1b1c61a05 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1085,6 +1085,7 @@ def measure( *, # Everything after here must be a kwarg label: Optional[str] = None, unit: Optional[str] = None, + setpoints: Optional[Union['Sweep', Sequence]] = None, timestamp: bool = False, **kwargs, ) -> Any: @@ -1105,6 +1106,7 @@ def measure( Otherwise, the default name is used. label: Optional label, is ignored if measurable is a Parameter or callable unit: Optional unit, is ignored if measurable is a Parameter or callable + setpoints: Optional setpoints if measuring an array, can be sequence or Sweep timestamp: If True, the timestamps immediately before and after this measurement are recorded @@ -1168,7 +1170,7 @@ def measure( elif isinstance(measurable, dict): result = self._measure_dict(measurable, name=name) elif isinstance(measurable, (list, np.ndarray)): - result = self._measure_array(measurable, name=name, setpoints=kwargs.get("setpoints")) + result = self._measure_array(measurable, name=name, setpoints=setpoints) elif isinstance(measurable, RAW_VALUE_TYPES): result = self._measure_value( measurable, name=name, label=label, unit=unit, **kwargs diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py index 8ab6cd5f9a5..e8e86360915 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_basics.py @@ -265,9 +265,86 @@ def test_measurement_fraction_complete(): assert msmt.fraction_complete() == round(0.5 + 0.05 * (k+1), 3) - def test_save_array_0D(): with MeasurementLoop('array_0D') as msmt: msmt.measure([1,2,3], 'array') - \ No newline at end of file + # Verify results + data = msmt.dataset.get_parameter_data('array')['array'] + assert 'array' in data + assert 'setpoint_idx' in data + assert list(data['array']) == [1,2,3] + assert list(data['setpoint_idx']) == [0, 1, 2] + + +def test_save_array_0D_custom_setpoint_list(): + with MeasurementLoop('array_0D') as msmt: + msmt.measure([1,2,3], 'array', setpoints=[3,4,5]) + + # Verify results + data = msmt.dataset.get_parameter_data('array')['array'] + assert 'array' in data + assert 'setpoint_idx' in data + assert list(data['array']) == [1,2,3] + assert list(data['setpoint_idx']) == [3, 4, 5] + + +def test_save_array_0D_custom_setpoint_sweep(): + with MeasurementLoop('array_0D') as msmt: + msmt.measure( + [1,2,3], 'array', + setpoints=Sweep([2,3,4], 'my_sweep')) + + # Verify results + data = msmt.dataset.get_parameter_data('array')['array'] + assert 'array' in data + assert 'my_sweep' in data + assert list(data['array']) == [1,2,3] + assert list(data['my_sweep']) == [2, 3, 4] + + +def test_save_array_1D(): + with MeasurementLoop('array_0D') as msmt: + for k in Sweep([5, 6], 'outer_sweep'): + msmt.measure([1,2,3], 'array') + + # Verify results + data = msmt.dataset.get_parameter_data('array')['array'] + assert 'array' in data + assert 'outer_sweep' in data + assert 'setpoint_idx' in data + np.testing.assert_array_equal(data['array'], [[1,2,3], [1,2,3]]) + np.testing.assert_array_equal(data['outer_sweep'], [[5,5,5], [6,6,6]]) + np.testing.assert_array_equal(data['setpoint_idx'], [[0,1,2], [0,1,2]]) + + +def test_save_array_1D_custom_setpoint_list(): + with MeasurementLoop('array_0D') as msmt: + for k in Sweep([5, 6], 'outer_sweep'): + msmt.measure([1,2,3], 'array', setpoints=[3,4,5]) + + # Verify results + data = msmt.dataset.get_parameter_data('array')['array'] + assert 'array' in data + assert 'outer_sweep' in data + assert 'setpoint_idx' in data + np.testing.assert_array_equal(data['array'], [[1,2,3], [1,2,3]]) + np.testing.assert_array_equal(data['outer_sweep'], [[5,5,5], [6,6,6]]) + np.testing.assert_array_equal(data['setpoint_idx'], [[3,4,5], [3,4,5]]) + + +def test_save_array_1D_custom_setpoint_sweep(): + with MeasurementLoop('array_0D') as msmt: + for k in Sweep([5, 6], 'outer_sweep'): + msmt.measure( + [1,2,3], 'array', + setpoints=Sweep([2,3,4], 'my_sweep', unit='V')) + + # Verify results + data = msmt.dataset.get_parameter_data('array')['array'] + assert 'array' in data + assert 'outer_sweep' in data + assert 'my_sweep' in data + np.testing.assert_array_equal(data['array'], [[1,2,3], [1,2,3]]) + np.testing.assert_array_equal(data['outer_sweep'], [[5,5,5], [6,6,6]]) + np.testing.assert_array_equal(data['my_sweep'], [[2,3,4], [2,3,4]]) \ No newline at end of file From f1f1516dc23a990d69bf97d482d37ea8825a4663 Mon Sep 17 00:00:00 2001 From: Serwan Date: Thu, 8 Dec 2022 08:37:52 +0100 Subject: [PATCH 113/122] don't show more than one tqdm progress bar --- qcodes/dataset/measurement_loop.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 83b5f54da3d..2d2568f1487 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -11,6 +11,7 @@ import numpy as np from tqdm import tqdm +from tqdm.notebook import tqdm as tqdm_notebook from qcodes.dataset.data_set_protocol import DataSetProtocol from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement @@ -717,7 +718,11 @@ def _get_maximum_action_index(self, action_indices, position): def _update_progress_bar(self, action_indices, description=None, create_if_new=True): # Register new progress bar if action_indices not in self.progress_bars: - if create_if_new: + # Do not create progress bar if one already exists and it's not a widget + # Otherwise stdout gets spammed + if not isinstance(tqdm, tqdm_notebook) and self.progress_bars: + return + elif create_if_new: self.progress_bars[action_indices] = tqdm( total=np.prod(self.loop_shape), desc=description, From f7fe1c336c7f02dd17131de8c2750ea5679675c6 Mon Sep 17 00:00:00 2001 From: Serwan Date: Thu, 8 Dec 2022 08:38:05 +0100 Subject: [PATCH 114/122] minor fix for measuring arrays --- qcodes/dataset/measurement_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 2d2568f1487..21aa1d0c096 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -272,9 +272,9 @@ def add_measurement_result( measurement_info["latest_value"] = result def get_result_setpoints(self, result, action_indices): + measurement_info = self.measurement_list[action_indices] # Check if result is an array if np.ndim(result) > 0: - measurement_info = self.measurement_list[action_indices] setpoints = [] for k, setpoint_indices in enumerate(measurement_info["setpoints_action_indices"]): From 6dad7e6182edb47eda071af31fbf2e84b09389d7 Mon Sep 17 00:00:00 2001 From: Serwan Date: Thu, 8 Dec 2022 08:38:14 +0100 Subject: [PATCH 115/122] bug in unmasking --- qcodes/dataset/measurement_loop.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 21aa1d0c096..f4595ea082e 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1229,7 +1229,7 @@ def _mask_attr(self, obj: object, attr: str, value) -> Any: self._masked_properties.append( { - "type": "attr", + "unmask_type": "attr", "obj": obj, "attr": attr, "original_value": original_value, @@ -1257,7 +1257,7 @@ def _mask_parameter(self, param: _BaseParameter, value: Any) -> Any: self._masked_properties.append( { - "type": "parameter", + "unmask_type": "parameter", "obj": param, "original_value": original_value, "value": value, @@ -1285,7 +1285,7 @@ def _mask_key(self, obj: dict, key: str, value: Any) -> Any: self._masked_properties.append( { - "type": "key", + "unmask_type": "key", "obj": obj, "key": key, "original_value": original_value, From ed1cb20eeb12e1e6f6dfa47b4af8f33166f6b56c Mon Sep 17 00:00:00 2001 From: Serwan Date: Thu, 8 Dec 2022 08:50:47 +0100 Subject: [PATCH 116/122] Allow sweep without name --- qcodes/dataset/measurement_loop.py | 63 ++----------------- .../test_measurement_loop_sweep.py | 18 +++--- 2 files changed, 14 insertions(+), 67 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index f4595ea082e..506d7af5962 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -2023,9 +2023,10 @@ def _transform_args_to_kwargs(self, *args, **kwargs) -> Tuple[dict]: """ if len(args) == 1: # Sweep([1,2,3], name="name") if isinstance(args[0], Iterable): - assert ( - kwargs.get("name") is not None - ), "Must provide name if sweeping iterable" + if kwargs.get("name") is None: + kwargs["name"] = "iteration" + if kwargs.get("label") is None: + kwargs["label"] = "Iteration" (kwargs["sequence"],) = args elif isinstance(args[0], _BaseParameter): assert ( @@ -2166,59 +2167,3 @@ def _generate_sequence( ) return sequence - - -class RepetitionSweep(BaseSweep): - """Basic sweep to repeat something multiple times - Its functionality is comparable to range(N) - - Args: - repetitions: Number of times to loop over - start: Starting index - name: Sweep name, defaults to "repetition" - label: Sweep label, defaults to "Repetition" - unit: Optional sweep unit - """ - - def __init__( - self, - repetitions: int, - start: int = 0, - name: str = "repetition", - label: str = "Repetition", - unit: Optional[str] = None, - ): - self.start = start - self.repetitions = repetitions - sequence = start + np.arange(repetitions) - - super().__init__(sequence, name, label, unit) - - -def measure_sweeps( - sweeps: List[BaseSweep], - measure_params: List[_BaseParameter], - msmt: "MeasurementLoop" = None, -) -> None: - """Recursively iterate over Sweep objects, measuring measure_params in innermost loop - - This method is used to perform arbitrary-dimension by passing a list of sweeps, - it can be compared to `dond` - - Args: - sweeps: list of BaseSweep objects to sweep over - measure_params: list of parameters to measure in innermost loop - """ - - if sweeps: - outer_sweep, *inner_sweeps = sweeps - - for _ in outer_sweep: - measure_sweeps(inner_sweeps, measure_params, msmt=msmt) - - else: - if msmt is None: - msmt = running_measurement() - - for measure_param in measure_params: - msmt.measure(measure_param) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index 1003ea43269..5be6bb9caa1 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -52,26 +52,27 @@ def test_sweep_2_args_parameter_stop(): # No initial value with pytest.raises(ValueError): - sweep = Sweep(sweep_parameter, 10) + sweep = Sweep(sweep_parameter, stop=10) with pytest.raises(ValueError): - sweep = Sweep(sweep_parameter, 10, num=21) + sweep = Sweep(sweep_parameter, stop=10, num=21) sweep_parameter(0) with pytest.raises(SyntaxError): sweep = Sweep(sweep_parameter, 10) - sweep = Sweep(sweep_parameter, 10, num=21) + sweep = Sweep(sweep_parameter, stop=10, num=21) assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) sweep_parameter.sweep_defaults = {"num": 21} - sweep = Sweep(sweep_parameter, 10) + sweep = Sweep(sweep_parameter, stop=10) assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) def test_sweep_2_args_sequence_name(): sweep_values = [1, 2, 3] - with pytest.raises(AssertionError): - sweep = Sweep(sweep_values) + sweep = Sweep(sweep_values) + assert sweep.name == 'iteration' + assert sweep.label == 'Iteration' sweep = Sweep(sweep_values, "sweep_values") assert np.allclose(sweep.sequence, sweep_values) @@ -87,8 +88,9 @@ def test_sweep_3_args_parameter_start_stop(): assert np.allclose(sweep.sequence, np.linspace(0, 10, 21)) sweep_values = [1, 2, 3] - with pytest.raises(AssertionError): - sweep = Sweep(sweep_values) + sweep = Sweep(sweep_values) + assert sweep.name == 'iteration' + assert sweep.label == 'Iteration' sweep = Sweep(sweep_values, "sweep_values") assert np.allclose(sweep.sequence, sweep_values) From cd950df384ed323a71861fb5efe0523a7034281d Mon Sep 17 00:00:00 2001 From: Serwan Date: Thu, 8 Dec 2022 09:21:43 +0100 Subject: [PATCH 117/122] Fix unmasking of sweep in msmt --- qcodes/dataset/measurement_loop.py | 60 ++++++++++++++++--- .../test_measurement_loop_sweep.py | 14 +++++ 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 506d7af5962..4e994dc5f33 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -1356,6 +1356,7 @@ def unmask( unmask_type: Optional[str] = None, value: Optional[Any] = None, raise_exception: bool = True, + remove_from_list: bool = True, **kwargs, # Add kwargs because original_value may be None ) -> None: """Unmasks a previously masked object, i.e. revert value back to original @@ -1367,6 +1368,8 @@ def unmask( type: can be 'key', 'attr', 'parameter' if not explicitly provided by kwarg value: Optional masked value, only used for logging raise_exception: Whether to raise exception if unmasking fails + remove_from_list: Whether to remove the masked property from the list + msmt._masked_properties. This ensures we don't unmask twice. """ if "original_value" not in kwargs: # No masked property passed. We collect all the masked properties @@ -1386,7 +1389,8 @@ def unmask( for unmask_property in reversed(unmask_properties): self.unmask(**unmask_property) - self._masked_properties = remaining_masked_properties + if remove_from_list: + self._masked_properties = remaining_masked_properties else: # A masked property has been passed, which we unmask here try: @@ -1407,6 +1411,20 @@ def unmask( obj(original_value) else: raise SyntaxError(f"Unmask type {unmask_type} not understood") + + # Try to find masked property and remove from list + if remove_from_list: + for masked_property in reversed(self._masked_properties): + if masked_property["obj"] != obj: + continue + elif attr is not None and masked_property.get("attr") != attr: + continue + elif key is not None and masked_property.get("key") != key: + continue + else: + self._masked_properties.remove(masked_property) + break + except Exception as e: self.log( f"Could not unmask {obj} {unmask_type} from masked value {value} " @@ -1688,12 +1706,6 @@ def __next__(self) -> Any: action_indices[-1] = 0 msmt.action_indices = tuple(action_indices) except StopIteration: # Reached end of iteration - if self.revert: - if isinstance(self.sequence, SweepValues): - msmt.unmask(self.sequence.parameter) - else: - # TODO: Check what other iterators might be able to be masked - pass self.exit_sweep() raise StopIteration @@ -1786,6 +1798,11 @@ def initialize(self) -> Dict[str, Any]: def exit_sweep(self) -> None: """Exits sweep, stepping out of the current `Measurement.action_indices`""" msmt = running_measurement() + if self.revert: + if isinstance(self.sequence, SweepValues): + msmt.unmask(self.sequence.parameter) + elif self.parameter is not None: + msmt.unmask(self.parameter) msmt.step_out(reduce_dimension=True) def execute( @@ -2167,3 +2184,32 @@ def _generate_sequence( ) return sequence + + +def measure_sweeps( + sweeps: List[BaseSweep], + measure_params: List[_BaseParameter], + msmt: "MeasurementLoop" = None, +) -> None: + """Recursively iterate over Sweep objects, measuring measure_params in innermost loop + + This method is used to perform arbitrary-dimension by passing a list of sweeps, + it can be compared to `dond` + + Args: + sweeps: list of BaseSweep objects to sweep over + measure_params: list of parameters to measure in innermost loop + """ + + if sweeps: + outer_sweep, *inner_sweeps = sweeps + + for _ in outer_sweep: + measure_sweeps(inner_sweeps, measure_params, msmt=msmt) + + else: + if msmt is None: + msmt = running_measurement() + + for measure_param in measure_params: + msmt.measure(measure_param) diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index 5be6bb9caa1..d311882a54d 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -183,3 +183,17 @@ def test_sweep_execute_sweep_args(): arr = dataset.get_parameter_data("get_param")["get_param"]["get_param"] assert np.allclose(arr, [[2, 3, 4], [3, 4, 5], [4, 5, 6]]) print(dataset) + + +def test_sweep_reverting(): + param = ManualParameter('param', initial_value=42) + with MeasurementLoop('test_revert') as msmt: + for val in Sweep(param, range(5), revert=True): + msmt.measure(val, 'value') + + print(msmt._masked_properties) + + assert param() == 42 + + param(41) + assert param() == 41 \ No newline at end of file From 4dd77cb1bd31483dff2ff958ba5988fc13ab8bf6 Mon Sep 17 00:00:00 2001 From: Serwan Date: Thu, 8 Dec 2022 09:40:28 +0100 Subject: [PATCH 118/122] add Iterate --- qcodes/dataset/__init__.py | 2 +- qcodes/dataset/measurement_loop.py | 49 +++++++++++++++++++ .../test_measurement_loop_sweep.py | 14 +++++- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/qcodes/dataset/__init__.py b/qcodes/dataset/__init__.py index 41a19ce24d6..c9ade67e464 100644 --- a/qcodes/dataset/__init__.py +++ b/qcodes/dataset/__init__.py @@ -29,7 +29,7 @@ ) from .experiment_settings import get_default_experiment_id, reset_default_experiment_id from .legacy_import import import_dat_file -from .measurement_loop import MeasurementLoop, Sweep +from .measurement_loop import MeasurementLoop, Sweep, Iterate from .measurements import Measurement from .plotting import plot_by_id, plot_dataset from .sqlite.connection import ConnectionPlus diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 4e994dc5f33..465fb008394 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -2213,3 +2213,52 @@ def measure_sweeps( for measure_param in measure_params: msmt.measure(measure_param) + + +class Iterate(Sweep): + """Variant of Sweep that is used to iterate outside a MeasurementLoop""" + def __iter__(self) -> Iterable: + # Determine sweep parameter + if self.parameter is None: + if isinstance(self.sequence, _IterateDondSweep): + # sweep is a doNd sweep that already has a parameter + self.parameter = self.sequence.parameter + else: + # Need to create a parameter + self.parameter = Parameter( + name=self.name, label=self.label, unit=self.unit + ) + + # We use this to revert back in the end + self.original_value = self.parameter.get() + + self.loop_index = 0 + self.dimension = 1 + self.iterator = iter(self.sequence) + + return self + + def __next__(self) -> Any: + try: # Perform loop action + sweep_value = next(self.iterator) + except StopIteration: # Reached end of iteration + if self.revert: + try: + self.parameter(self.original_value) + except Exception: + warn(f'Could not revert {self.parameter} to {self.original_value}') + raise StopIteration + + # Set parameter if passed along + if self.parameter is not None and self.parameter.settable: + self.parameter(sweep_value) + + # Optional wait after settings value + if self.initial_delay and self.loop_index == 0: + sleep(self.initial_delay) + if self.delay: + sleep(self.delay) + + self.loop_index += 1 + + return sweep_value \ No newline at end of file diff --git a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py index d311882a54d..a867c08eb6a 100644 --- a/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py +++ b/qcodes/tests/dataset/measurement_loop/test_measurement_loop_sweep.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from qcodes.dataset import LinSweep, MeasurementLoop, Sweep, dond +from qcodes.dataset import LinSweep, MeasurementLoop, Sweep, dond, Iterate from qcodes.instrument import ManualParameter, Parameter @@ -196,4 +196,14 @@ def test_sweep_reverting(): assert param() == 42 param(41) - assert param() == 41 \ No newline at end of file + assert param() == 41 + + +def test_iterate(): + param = ManualParameter('param', initial_value=42) + + expected_vals = np.linspace(37, 47, 21) + for k, val in enumerate(Iterate(param, around=5, num=21)): + assert val == expected_vals[k] + + assert param() == 42 \ No newline at end of file From 9c34a28b53107fdb9c436c63db8cbccd2eb5dd44 Mon Sep 17 00:00:00 2001 From: Serwan Date: Sun, 18 Dec 2022 10:20:49 +0100 Subject: [PATCH 119/122] Add N-dimensional array support --- qcodes/dataset/measurement_loop.py | 75 +++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 465fb008394..a73d2cb2ff1 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -275,20 +275,38 @@ def get_result_setpoints(self, result, action_indices): measurement_info = self.measurement_list[action_indices] # Check if result is an array if np.ndim(result) > 0: - setpoints = [] + if len(measurement_info["setpoints_action_indices"]) < np.ndim(result): + raise ValueError( + f"Number of setpoints {len(measurement_info['setpoints_action_indices'])} " + f"is less than array dimensionality {np.ndim(result)}" + ) - for k, setpoint_indices in enumerate(measurement_info["setpoints_action_indices"]): + # Pick the last N sweeps, where N is the array dimensionality + setpoints_action_indices = measurement_info["setpoints_action_indices"] + repeat_setpoints_action_indices = setpoints_action_indices[:-np.ndim(result)] + mesh_setpoints_action_indices = setpoints_action_indices[-np.ndim(result):] + + # Create repetitions of outer setpoints + repeat_setpoint_arrs = [] + for k, setpoint_indices in enumerate(repeat_setpoints_action_indices): + latest_value = self.setpoint_list[setpoint_indices]["latest_value"] + setpoint_arr = np.tile(latest_value, np.shape(result)) + repeat_setpoint_arrs.append(setpoint_arr) + + # Create mesh from last N setpoints matching + mesh_setpoint_arrs = [] + for k, setpoint_indices in enumerate(mesh_setpoints_action_indices): setpoint_info = self.setpoint_list[setpoint_indices] - if k < len(measurement_info["setpoints_action_indices"]) - 1: - # Inner setpoints, repeat value by length of array - latest_value = setpoint_info["latest_value"] - sweep_arr = np.repeat(latest_value, len(result)) - else: - # Innermost loop, get sequence - sweep_arr = setpoint_info["sweep"].sequence - assert len(sweep_arr) == len(result) + sequence = setpoint_info["sweep"].sequence + mesh_setpoint_arrs.append(sequence) + if len(sequence) != np.shape(result)[k]: + raise ValueError( + f'Setpoint {k} {setpoint_info["sweep"].name} length differs ' + f'from dimension {k} of array: {len(sequence)=} != {np.shape(result)[k]=}' + ) - setpoints.append(sweep_arr) + # Convert all 1D setpoint arrays to an N-D meshgrid + setpoints = repeat_setpoint_arrs + list(np.meshgrid(*mesh_setpoint_arrs, indexing='ij')) else: setpoints = [ self.setpoint_list[action_indices]["latest_value"] @@ -975,18 +993,38 @@ def _measure_array( unit: str = None, setpoints: 'Sweep' = None ): + # Determine + ndim = np.ndim(array) + + setpoints_list = [] + # Ensure setpoints is a Sweep if setpoints is None: - setpoints_sequence = range(len(array)) - setpoints = Sweep(setpoints_sequence, name='setpoint_idx', label='Setpoint index') + # Create setpoints for each dimension + for dim, num in enumerate(np.shape(array)): + sweep = Sweep( + range(num), + name='setpoint_idx' + (f'_{dim}' if np.ndim(array) > 1 else ''), + label='Setpoint index' + (f' dim_{dim}' if np.ndim(array) > 1 else '') + ) + setpoints_list.append(sweep) + elif isinstance(setpoints, Sweep): + # Setpoints is a single Sweep + assert ndim == 1 + assert len(setpoints) == len(array) + setpoints_list = [setpoints] elif isinstance(setpoints, (list, np.ndarray)): - # Convert sequence to Sweep - setpoints = Sweep(setpoints, name='setpoint_idx', label='Setpoint index') - elif not isinstance(setpoints, Sweep): + if isinstance(setpoints[0], Sweep): + setpoints_list = setpoints + else: + # Convert sequence to Sweep + setpoints_list = [Sweep(setpoints, name='setpoint_idx', label='Setpoint index')] + else: raise SyntaxError('Cannot measure because array setpoints not understood') # Enter sweep - iter(setpoints) + for setpoints in setpoints_list: + iter(setpoints) # Ensure measuring array matches the current action_indices self._verify_action(action=None, name=name, add_if_new=True) @@ -1000,7 +1038,8 @@ def _measure_array( unit=unit, ) - setpoints.exit_sweep() + for setpoints in reversed(setpoints_list): + setpoints.exit_sweep() def _measure_dict(self, value: dict, name: str) -> Dict[str, Any]: """Store dictionary results From b79b2b5df1d11a8455f2bce61688cea8772b2361 Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Mon, 19 Dec 2022 09:45:03 +0100 Subject: [PATCH 120/122] improvements to config --- qcodes/configuration/config.py | 113 +++++++++++++++++++++++---------- 1 file changed, 79 insertions(+), 34 deletions(-) diff --git a/qcodes/configuration/config.py b/qcodes/configuration/config.py index 05f251ea51e..d6d72f13f2b 100644 --- a/qcodes/configuration/config.py +++ b/qcodes/configuration/config.py @@ -403,13 +403,18 @@ def describe(self, name: str) -> str: return doc - def __getitem__(self, name: str) -> Any: - val = self.current_config - for key in name.split('.'): - if val is None: - raise KeyError(f"{name} not found in current config") - val = val[key] - return val + def __getitem__(self, name: Union[int,str]) -> Any: + if isinstance(name, int): + # Integer requested, likely a consequence of "if 'string' in config" + # Return key corresponding to index + return list(self.current_config.keys())[name] + else: + val = self.current_config + for key in name.split('.'): + if val is None: + raise KeyError(f"{name} not found in current config") + val = val[key] + return val def __getattr__(self, name: str) -> Any: return getattr(self.current_config, name) @@ -422,22 +427,21 @@ def __repr__(self) -> str: return output -class DotDict(Dict[str, Any]): +class DotDict(dict): """ Wrapper dict that allows to get dotted attributes - - Requires keys to be strings. """ - - def __init__(self, value: Mapping[str, Any] | None = None): + exclude_from_dict = [] + def __init__(self, value=None): if value is None: pass else: for key in value: self.__setitem__(key, value[key]) - def __setitem__(self, key: str, value: Any) -> None: - if '.' in key: + def __setitem__(self, key, value): + # string type must be checked, as key could be other datatype + if type(key)==str and '.' in key: myKey, restOfKey = key.split('.', 1) target = self.setdefault(myKey, DotDict()) target[restOfKey] = value @@ -446,36 +450,77 @@ def __setitem__(self, key: str, value: Any) -> None: value = DotDict(value) dict.__setitem__(self, key, value) - def __getitem__(self, key: str) -> Any: - if '.' not in key: + def __getitem__(self, key): + if type(key) != str or '.' not in key: return dict.__getitem__(self, key) myKey, restOfKey = key.split('.', 1) target = dict.__getitem__(self, myKey) return target[restOfKey] - def __contains__(self, key: object) -> bool: - if not isinstance(key, str): - return False - if '.' not in key: - return super().__contains__(key) + def __contains__(self, key): + if not isinstance(key, str) or '.' not in key: + return dict.__contains__(self, key) myKey, restOfKey = key.split('.', 1) - target = dict.__getitem__(self, myKey) - return restOfKey in target - def __deepcopy__(self, memo: dict[Any, Any] | None) -> DotDict: - return DotDict(copy.deepcopy(dict(self))) + if myKey not in self: + return False + else: + target = dict.__getitem__(self, myKey) + return restOfKey in target - def __getattr__(self, name: str) -> Any: - """ - Overwrite ``__getattr__`` to provide dot access - """ - return self.__getitem__(name) + def __deepcopy__(self, memo): + return DotDict(copy.deepcopy(dict(self))) - def __setattr__(self, key: str, value: Any) -> None: - """ - Overwrite ``__setattr__`` to provide dot access + # dot acces baby + def __setattr__(self, key, val): + if key in self.exclude_from_dict: + self.__dict__[key] = val + else: + self.__setitem__(key, val) + + def __getattr__(self, key): + try: + return self.__getitem__(key) + except KeyError: + raise AttributeError(f'Attribute {key} not found') + + def __dir__(self): + # Add keys to dir, used for auto-completion + items = super().__dir__() + items.extend(self.keys()) + return items + + def setdefault(self, key, default=None): + """Set value of a key if it does not yet exist""" + d = self + if isinstance(key, str): + *parent_keys, key = key.split('.') + for subkey in parent_keys: + d = dict.setdefault(d, subkey, DotDict()) + + return dict.setdefault(d, key, default) + + def create_dicts(self, *keys): + """Create nested dict structure + Args: + *keys: Sequence of key strings. Empty DotDicts will be created if + each key does not yet exist + Returns: + Most inner dict, newly created if it does not yet exist + Examples: + d = DotDict() + d.create_dicts('a', 'b', 'c') + print(d.a.b.c) + >>> {} """ - self.__setitem__(key, value) + d = self + for key in keys: + if key in self: + assert isinstance(d[key], dict) + + d.setdefault(key, DotDict()) + d = d[key] + return d def update(d: dict[Any, Any], u: Mapping[Any, Any]) -> dict[Any, Any]: From 54aad18a7d6b9bbb3bbf3c493e7536fd4a5b18d6 Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Mon, 19 Dec 2022 09:45:15 +0100 Subject: [PATCH 121/122] verification improvements --- qcodes/dataset/measurement_loop.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index a73d2cb2ff1..881ec54b29d 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -640,10 +640,6 @@ def __exit__(self, exc_type: Exception, exc_val, exc_tb) -> None: # include final metadata t_stop = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.dataset.add_metadata("t_stop", t_stop) - self.dataset.add_metadata( - "timings", - json.dumps(dict(self.timings.timings), cls=NumpyJSONEncoder) - ) self.data_handler.finalize() self.log("Measurement finished") @@ -960,14 +956,18 @@ def _measure_callable( action_indices_str = "_".join(str(idx) for idx in self.action_indices) name = f"data_group_{action_indices_str}" - # Ensure measuring callable matches the current action_indices - self._verify_action(action=measurable_function, name=name, add_if_new=True) - # Record action_indices before the callable is called action_indices = self.action_indices results = measurable_function(**kwargs) + if self.action_indices != action_indices: + # Measurements have been performed in this function, don't measure anymore + return + + # Ensure measuring callable matches the current action_indices + self._verify_action(action=measurable_function, name=name, add_if_new=True) + # Check if the callable already performed a nested measurement # In this case, the nested measurement is stored as a data_group, and # has loop indices corresponding to the current ones. From 6300b17eb019712195da6441b315d7089bd60fbb Mon Sep 17 00:00:00 2001 From: The Beefy One Date: Tue, 7 Mar 2023 11:17:40 +0100 Subject: [PATCH 122/122] add thread functionality --- qcodes/dataset/measurement_loop.py | 65 +++++++++++++++++++++++++----- qcodes/parameters/parameter.py | 2 + 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/qcodes/dataset/measurement_loop.py b/qcodes/dataset/measurement_loop.py index 881ec54b29d..1817b79ea16 100644 --- a/qcodes/dataset/measurement_loop.py +++ b/qcodes/dataset/measurement_loop.py @@ -3,6 +3,7 @@ import logging import threading import traceback +import concurrent from datetime import datetime from time import perf_counter, sleep from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union @@ -1008,14 +1009,21 @@ def _measure_array( label='Setpoint index' + (f' dim_{dim}' if np.ndim(array) > 1 else '') ) setpoints_list.append(sweep) - elif isinstance(setpoints, Sweep): + elif isinstance(setpoints, BaseSweep): # Setpoints is a single Sweep assert ndim == 1 assert len(setpoints) == len(array) + if isinstance(setpoints, Iterate): + setpoints = setpoints.convert_to_Sweep() + setpoints_list = [setpoints] elif isinstance(setpoints, (list, np.ndarray)): - if isinstance(setpoints[0], Sweep): - setpoints_list = setpoints + if isinstance(setpoints[0], BaseSweep): + setpoints_list = [] + for setpoint in setpoints: + if isinstance(setpoint, Iterate): + setpoint = setpoint.convert_to_Sweep() + setpoints_list.append(setpoint) else: # Convert sequence to Sweep setpoints_list = [Sweep(setpoints, name='setpoint_idx', label='Setpoint index')] @@ -1089,7 +1097,10 @@ def _measure_value( unit: Optional unit for data array """ if name is None: - raise RuntimeError("Must provide a name when measuring a value") + if parameter is not None: + name = parameter.name + else: + raise RuntimeError("Must provide a name when measuring a value") # Ensure measuring callable matches the current action_indices self._verify_action(action=None, name=name, add_if_new=True) @@ -1166,7 +1177,7 @@ def measure( # DataGroup in the running measurement. Delegate measurement to the # running measurement return MeasurementLoop.running_measurement.measure( - measurable, name=name, label=label, unit=unit, **kwargs + measurable, name=name, label=label, unit=unit, setpoints=setpoints, **kwargs ) # Code from hereon is only reached by the primary measurement, @@ -1247,6 +1258,19 @@ def measure( return result + def measure_threaded(self, params): + if all(isinstance(param, Parameter) for param in params): + with concurrent.futures.ThreadPoolExecutor() as executor: + threads = [executor.submit(param) for param in params] + + results = [thread.result() for thread in threads] + + for param, result in zip(params, results): + self.measure(result, parameter=param) + else: + results = [self.measure(param) for param in params] + return results + # Methods related to masking of parameters/attributes/keys def _mask_attr(self, obj: object, attr: str, value) -> Any: """Temporarily override an object attribute during the measurement. @@ -1626,6 +1650,7 @@ class BaseSweep(AbstractSweep): ``` """ plot_function = None + DEFAULT_MEASURE_THREADED = False def __init__( self, @@ -1851,6 +1876,7 @@ def execute( measure_params: Union[Iterable, _BaseParameter] = None, repetitions: int = 1, sweep: Union[Iterable, "BaseSweep"] = None, + thread=None, plot: bool = False, ) -> DataSetProtocol: """Performs a measurement using this sweep @@ -1881,6 +1907,9 @@ def execute( ) measure_params = station.measure_params + if thread is None: + thread = self.DEFAULT_MEASURE_THREADED + # Convert measure_params to list if it is a single param if isinstance(measure_params, _BaseParameter): measure_params = [measure_params] @@ -1910,7 +1939,7 @@ def execute( name = f"{dimensionality}D_sweep_" + "_".join(sweep_names) with MeasurementLoop(name) as msmt: - measure_sweeps(sweeps=sweeps, measure_params=measure_params, msmt=msmt) + measure_sweeps(sweeps=sweeps, measure_params=measure_params, msmt=msmt, thread=thread) if plot and Sweep.plot_function is not None and MeasurementLoop.running_measurement is None: Sweep.plot_function(msmt.dataset) @@ -2229,6 +2258,7 @@ def measure_sweeps( sweeps: List[BaseSweep], measure_params: List[_BaseParameter], msmt: "MeasurementLoop" = None, + thread=False, ) -> None: """Recursively iterate over Sweep objects, measuring measure_params in innermost loop @@ -2244,14 +2274,17 @@ def measure_sweeps( outer_sweep, *inner_sweeps = sweeps for _ in outer_sweep: - measure_sweeps(inner_sweeps, measure_params, msmt=msmt) + measure_sweeps(inner_sweeps, measure_params, msmt=msmt, thread=thread) else: if msmt is None: msmt = running_measurement() - for measure_param in measure_params: - msmt.measure(measure_param) + if thread: + msmt.measure_threaded(measure_params) + else: + for measure_param in measure_params: + msmt.measure(measure_param) class Iterate(Sweep): @@ -2300,4 +2333,16 @@ def __next__(self) -> Any: self.loop_index += 1 - return sweep_value \ No newline at end of file + return sweep_value + + def convert_to_Sweep(self): + return BaseSweep( + sequence=self.sequence, + name=self.name, + label=self.label, + unit=self.unit, + parameter=self.parameter, + revert=self.revert, + delay=self.delay, + initial_delay=self.initial_delay + ) \ No newline at end of file diff --git a/qcodes/parameters/parameter.py b/qcodes/parameters/parameter.py index cdb75f88461..eabad42295d 100644 --- a/qcodes/parameters/parameter.py +++ b/qcodes/parameters/parameter.py @@ -415,6 +415,7 @@ def sweep( measure_params: ParameterBase=None, repetitions: int = 1, sweep=None, + thread=None, plot: bool = None, ): """Perform a measurement by sweeping this parameter @@ -493,6 +494,7 @@ def sweep( repetitions=repetitions, sweep=sweep, plot=plot, + thread=thread, ) return dataset