diff --git a/.github/lazy-imports/sitecustomize.py b/.github/lazy-imports/sitecustomize.py new file mode 100644 index 000000000000..55959c277f49 --- /dev/null +++ b/.github/lazy-imports/sitecustomize.py @@ -0,0 +1,16 @@ +"""Limit globally enabled lazy imports to imports involving QCoDeS.""" + +# ruff: noqa: INP001 + +import sys + + +def qcodes_import_filter( + importing: str | None, imported: str, fromlist: tuple[str, ...] | None +) -> bool: + return imported.startswith("qcodes") or ( + importing is not None and importing.startswith("qcodes") + ) + + +sys.set_lazy_imports_filter(qcodes_import_filter) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index ed613c3d52f3..40e0e2c68a82 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -46,6 +46,8 @@ jobs: env: OS: ${{ matrix.os }} PYTHON: ${{ matrix.python-version }} + PYTHON_LAZY_IMPORTS: ${{ matrix.python-version == '3.15' && 'all' || '' }} + PYTHONPATH: ${{ matrix.python-version == '3.15' && format('{0}/.github/lazy-imports', github.workspace) || '' }} steps: diff --git a/docs/changes/newsfragments/8600.breaking b/docs/changes/newsfragments/8600.breaking new file mode 100644 index 000000000000..6d798a703103 --- /dev/null +++ b/docs/changes/newsfragments/8600.breaking @@ -0,0 +1,16 @@ +To support Python 3.15 lazy imports, modules whose names collide with objects exported +from their parent package have been moved to private modules. This affects +``qcodes.dataset.dond`` and its submodules, ``qcodes.utils.full_class``, the Stanford Research +driver modules (e.g. ``qcodes.instrument_drivers.stanford_research.SR830``) and the Basel +driver modules (e.g. ``qcodes.instrument_drivers.basel.BaselSP983``). The old module paths +remain available as deprecated aliases that emit a ``QCoDeSDeprecationWarning`` and will be +removed in a future release. Import the objects from the parent package instead, e.g. +``from qcodes.instrument_drivers.stanford_research import SR830`` or +``from qcodes.dataset import dond``. + +Any other names that were importable from these modules but are not exported from the parent +package are now considered private. This includes the type aliases such as ``ParamMeasT`` and +``ActionsT``, as well as ``DondKWargs``, ``ParameterGroup``, ``ParameterSetEvent`` and +``catch_interrupts`` from ``qcodes.dataset.dond``. They remain importable from the deprecated +module aliases until those are removed. If you rely on one of these names, please open an issue +at https://github.com/microsoft/Qcodes/issues so that we can consider making it public. diff --git a/docs/changes/newsfragments/8600.breaking.1 b/docs/changes/newsfragments/8600.breaking.1 new file mode 100644 index 000000000000..27d55c1e7091 --- /dev/null +++ b/docs/changes/newsfragments/8600.breaking.1 @@ -0,0 +1,7 @@ +``VoltageParameter`` from the SR560 driver has been renamed to ``SR560VoltageParameter`` and is now +exported from ``qcodes.instrument_drivers.stanford_research``. The parameter classes +``ChannelBuffer``, ``ChannelTrace`` and ``GeneratedSetPoints`` of the SR830 driver have been +renamed to ``SR830ChannelBuffer``, ``SR830ChannelTrace`` and ``SR830GeneratedSetPoints``. These +and ``SR86xBufferReadout`` of the SR86x drivers are only created by the drivers themselves and are +now private. The old names remain available from the deprecated ``SR560``, ``SR830`` and ``SR86x`` +module aliases until those are removed. diff --git a/docs/changes/newsfragments/8600.improved b/docs/changes/newsfragments/8600.improved new file mode 100644 index 000000000000..2fbfc8ba47e8 --- /dev/null +++ b/docs/changes/newsfragments/8600.improved @@ -0,0 +1,4 @@ +``do2d_retrace`` is now exported from ``qcodes.dataset``. It has also been fixed: previously it +failed because the setpoints of the outer sweep were not added to the datasets. Parameters +given as ``additional_setpoints`` are now registered in the datasets, and ``exit_actions`` are +called once at the end of the measurement rather than after every step of the outer sweep. diff --git a/docs/examples/writing_drivers/Creating-Instrument-Drivers.ipynb b/docs/examples/writing_drivers/Creating-Instrument-Drivers.ipynb index c6f5faa07345..b6a6aeefe3c9 100644 --- a/docs/examples/writing_drivers/Creating-Instrument-Drivers.ipynb +++ b/docs/examples/writing_drivers/Creating-Instrument-Drivers.ipynb @@ -851,7 +851,7 @@ "- example: `instrument_drivers.AlazarTech.ATS9870.AlazarTech_ATS9870`\n", "\n", "Although the class name can be just the model if it is globally unambiguous. For example:\n", - "- example: `instrument_drivers.stanford_research.SR560.SR560`\n", + "- example: `instrument_drivers.stanford_research.SR560`\n", "\n", "And note that due to mergers, some drivers may not be in the folder you expect:\n", "- example: `instrument_drivers.tektronix.Keithley_2600.Keithley_2600_Channels`" diff --git a/src/qcodes/dataset/__init__.py b/src/qcodes/dataset/__init__.py index 9f87792ede92..ebfc684476db 100644 --- a/src/qcodes/dataset/__init__.py +++ b/src/qcodes/dataset/__init__.py @@ -3,6 +3,15 @@ and from disk """ +from qcodes.utils.deprecate import _keep_attributes_shadowing_submodules + +from ._dond.do2d_retrace import do2d_retrace +from ._dond.do_0d import do0d +from ._dond.do_1d import do1d +from ._dond.do_2d import do2d +from ._dond.do_nd import dond +from ._dond.do_nd_utils import BreakConditionInterrupt +from ._dond.sweeps import AbstractSweep, ArraySweep, LinSweep, LogSweep, TogetherSweep from ._raw_data_storage import ( cleanup_datasets, purge_orphaned_datasets, @@ -26,12 +35,6 @@ from .descriptions.param_spec import ParamSpec from .descriptions.rundescriber import RunDescriber from .descriptions.versioning.serialization import rundescriber_from_json -from .dond.do_0d import do0d -from .dond.do_1d import do1d -from .dond.do_2d import do2d -from .dond.do_nd import dond -from .dond.do_nd_utils import BreakConditionInterrupt -from .dond.sweeps import AbstractSweep, ArraySweep, LinSweep, LogSweep, TogetherSweep from .experiment_container import ( experiments, load_experiment, @@ -71,6 +74,11 @@ call_params_threaded, ) +# Keep the public objects when the deprecated module aliases that share +# their names are imported. +_keep_attributes_shadowing_submodules(__name__) + + __all__ = [ "AbstractSweep", "ArraySweep", @@ -100,6 +108,7 @@ "do0d", "do1d", "do2d", + "do2d_retrace", "dond", "dond_into", "experiments", diff --git a/src/qcodes/dataset/_dond/__init__.py b/src/qcodes/dataset/_dond/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/qcodes/dataset/_dond/do2d_retrace.py b/src/qcodes/dataset/_dond/do2d_retrace.py new file mode 100644 index 000000000000..91c588c86526 --- /dev/null +++ b/src/qcodes/dataset/_dond/do2d_retrace.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from opentelemetry import trace + +from qcodes import config +from qcodes.dataset._dond.sweeps import LinSweep +from qcodes.dataset.measurement_extensions import ( + DataSetDefinition, + LinSweeper, + datasaver_builder, + dond_into, +) + +LOG = logging.getLogger(__name__) +TRACER = trace.get_tracer(__name__) + + +if TYPE_CHECKING: + from collections.abc import Sequence + + from qcodes.dataset._dond.do_nd_utils import ActionsT + from qcodes.dataset.data_set_protocol import DataSetProtocol + from qcodes.dataset.experiment_container import Experiment + from qcodes.parameters import ParameterBase + + +@TRACER.start_as_current_span("qcodes.dataset.do2d") +def do2d_retrace( + param_set1: ParameterBase, + start1: float, + stop1: float, + num_points1: int, + delay1: float, + param_set2: ParameterBase, + start2: float, + stop2: float, + num_points2: int, + delay2: float, + *param_meas: ParameterBase, + enter_actions: ActionsT = (), + exit_actions: ActionsT = (), + before_inner_actions: ActionsT = (), + after_inner_actions: ActionsT = (), + measurement_name: str = "", + exp: Experiment | None = None, + additional_setpoints: Sequence[ParameterBase] = (), + show_progress: bool | None = None, +) -> tuple[DataSetProtocol, DataSetProtocol]: + """ + Perform a 1D scan of ``param_set1`` from ``start1`` to ``stop1`` in + ``num_points1`` and ``param_set2`` from ``start2`` to ``stop2`` in + ``num_points2`` measuring param_meas at each step. + + Args: + param_set1: The QCoDeS parameter to sweep over in the outer loop + start1: Starting point of sweep in outer loop + stop1: End point of sweep in the outer loop + num_points1: Number of points to measure in the outer loop + delay1: Delay after setting parameter in the outer loop + param_set2: The QCoDeS parameter to sweep over in the inner loop + start2: Starting point of sweep in inner loop + stop2: End point of sweep in the inner loop + num_points2: Number of points to measure in the inner loop + delay2: Delay after setting parameter before measurement is performed + param_meas: Parameter(s) to measure at each step or functions that + will be called at each step. The function should take no arguments. + The parameters and functions are called in the order they are + supplied. + enter_actions: A list of functions taking no arguments that will be + called before the measurements start + exit_actions: A list of functions taking no arguments that will be + called after the measurements ends + before_inner_actions: Actions executed before each run of the inner loop + after_inner_actions: Actions executed after each run of the inner loop + measurement_name: Name of the measurement. This will be passed down to + the dataset produced by the measurement. If not given, a default + value of 'results' is used for the dataset. + exp: The experiment to use for this measurement. + additional_setpoints: A list of setpoint parameters to be registered in + the measurement but not scanned. + show_progress: should a progress bar be displayed during the + measurement. If None the setting will be read from ``qcodesrc.json`` + + Returns: + The QCoDeS dataset. + + """ + + if show_progress is None: + show_progress = config.dataset.dond_show_progress + + independent = [param_set1, param_set2, *additional_setpoints] + # the outer parameter is set by the LinSweeper below rather than swept by + # dond_into so it is recorded as an additional setpoint of the inner sweeps + inner_additional_setpoints = (param_set1, *additional_setpoints) + + dataset_definition = [ + DataSetDefinition( + name=measurement_name, + independent=independent, + dependent=param_meas, + experiment=exp, + ), + DataSetDefinition( + name=f"retrace {measurement_name}", + independent=independent, + dependent=param_meas, + experiment=exp, + ), + ] + + with datasaver_builder(dataset_definition) as datasavers: + for action in enter_actions: + action() + for _ in LinSweeper(param_set1, start1, stop1, num_points1, delay1): + sweep_up = LinSweep( + param_set2, + start2, + stop2, + num_points2, + delay2, + post_actions=after_inner_actions, + ) + sweep_down = LinSweep( + param_set2, + stop2, + start2, + num_points2, + delay2, + post_actions=after_inner_actions, + ) + for action in before_inner_actions: + action() + dond_into( + datasavers[0], + sweep_up, + *param_meas, + additional_setpoints=inner_additional_setpoints, + ) + for action in before_inner_actions: + action() + dond_into( + datasavers[1], + sweep_down, + *param_meas, + additional_setpoints=inner_additional_setpoints, + ) + for action in exit_actions: + action() + datasets = (datasavers[0].dataset, datasavers[1].dataset) + return datasets diff --git a/src/qcodes/dataset/_dond/do_0d.py b/src/qcodes/dataset/_dond/do_0d.py new file mode 100644 index 000000000000..ff13871b4f3f --- /dev/null +++ b/src/qcodes/dataset/_dond/do_0d.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, cast + +from opentelemetry import trace + +from .do_nd import DondKWargs, dond + +if TYPE_CHECKING: + from typing import Unpack + + from .do_nd_utils import ( + AxesTupleListWithDataSet, + ParamMeasT, + ) + +LOG = logging.getLogger(__name__) +TRACER = trace.get_tracer(__name__) + + +@TRACER.start_as_current_span("qcodes.dataset.do0d") +def do0d( + *param_meas: ParamMeasT, **kwargs: Unpack[DondKWargs] +) -> AxesTupleListWithDataSet: + """ + Perform a measurement of a single parameter. This is probably most + useful for a ParameterWithSetpoints that already returns an array of data points. + + Args: + *param_meas: Parameter(s) to measure at each step or functions that + will be called at each step. The function should take no arguments. + The parameters and functions are called in the order they are + supplied. + **kwargs: kwargs are the same as for :func:`dond` and forwarded directly to :func:`dond`. + + Returns: + The QCoDeS dataset. + + """ + + kwargs.setdefault("log_info", "Using 'qcodes.dataset.do0d'") + + # since we only support entering parameters + # as a simple list or args we are sure to always + # get back a AxesTupleListWithDataSet and cast is safe + return cast( + "AxesTupleListWithDataSet", + dond(*param_meas, **kwargs, squeeze=True), + ) diff --git a/src/qcodes/dataset/_dond/do_1d.py b/src/qcodes/dataset/_dond/do_1d.py new file mode 100644 index 000000000000..7852bffe1127 --- /dev/null +++ b/src/qcodes/dataset/_dond/do_1d.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, cast + +from opentelemetry import trace + +from .do_nd import DondKWargs, dond +from .sweeps import LinSweep + +if TYPE_CHECKING: + from typing import Unpack + + from qcodes.dataset._dond.do_nd_utils import ( + AxesTupleListWithDataSet, + ParamMeasT, + ) + from qcodes.parameters import ParameterBase + +LOG = logging.getLogger(__name__) +TRACER = trace.get_tracer(__name__) + + +@TRACER.start_as_current_span("qcodes.dataset.do1d") +def do1d( + param_set: ParameterBase, + start: float, + stop: float, + num_points: int, + delay: float, + *param_meas: ParamMeasT, + **kwargs: Unpack[DondKWargs], +) -> AxesTupleListWithDataSet: + """ + Perform a 1D scan of ``param_set`` from ``start`` to ``stop`` in + ``num_points`` measuring param_meas at each step. In case param_meas is + an ArrayParameter this is effectively a 2d scan. + + Args: + param_set: The QCoDeS parameter to sweep over + start: Starting point of sweep + stop: End point of sweep + num_points: Number of points in sweep + delay: Delay after setting parameter before measurement is performed + *param_meas: Parameter(s) to measure at each step or functions that + will be called at each step. The function should take no arguments. + The parameters and functions are called in the order they are + supplied. + **kwargs: kwargs are the same as for :func:`dond` and forwarded directly to :func:`dond`. + + Returns: + The QCoDeS dataset. + + """ + kwargs.setdefault("log_info", "Using 'qcodes.dataset.do1d'") + + return cast( + "AxesTupleListWithDataSet", + dond( + LinSweep( + param=param_set, + start=start, + stop=stop, + delay=delay, + num_points=num_points, + ), + *param_meas, + **kwargs, + squeeze=True, + ), + ) diff --git a/src/qcodes/dataset/_dond/do_2d.py b/src/qcodes/dataset/_dond/do_2d.py new file mode 100644 index 000000000000..2146d32b1bad --- /dev/null +++ b/src/qcodes/dataset/_dond/do_2d.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +import logging +import sys +import time +from typing import TYPE_CHECKING, cast + +import numpy as np +from opentelemetry import trace +from tqdm.auto import tqdm + +from qcodes import config +from qcodes.dataset._dond.do_nd_utils import ( + BreakConditionInterrupt, + _handle_plotting, + _register_actions, + _register_parameters, + _set_write_period, + catch_interrupts, +) +from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement +from qcodes.dataset.measurements import Measurement +from qcodes.dataset.threading import ( + SequentialParamsCaller, + ThreadPoolParamsCaller, + process_params_meas, +) +from qcodes.parameters import ParameterBase + +LOG = logging.getLogger(__name__) +TRACER = trace.get_tracer(__name__) + + +if TYPE_CHECKING: + from collections.abc import Sequence + + from qcodes.dataset._dond.do_nd_utils import ( + ActionsT, + AxesTupleListWithDataSet, + BreakConditionT, + ParamMeasT, + ) + from qcodes.dataset.descriptions.versioning.rundescribertypes import Shapes + from qcodes.dataset.experiment_container import Experiment + + +@TRACER.start_as_current_span("qcodes.dataset.do2d") +def do2d( + param_set1: ParameterBase, + start1: float, + stop1: float, + num_points1: int, + delay1: float, + param_set2: ParameterBase, + start2: float, + stop2: float, + num_points2: int, + delay2: float, + *param_meas: ParamMeasT, + set_before_sweep: bool | None = True, + enter_actions: ActionsT = (), + exit_actions: ActionsT = (), + before_inner_actions: ActionsT = (), + after_inner_actions: ActionsT = (), + write_period: float | None = None, + measurement_name: str = "", + exp: Experiment | None = None, + flush_columns: bool = False, + do_plot: bool | None = None, + use_threads: bool | None = None, + additional_setpoints: Sequence[ParameterBase] = (), + show_progress: bool | None = None, + log_info: str | None = None, + break_condition: BreakConditionT | None = None, +) -> AxesTupleListWithDataSet: + """ + Perform a 1D scan of ``param_set1`` from ``start1`` to ``stop1`` in + ``num_points1`` and ``param_set2`` from ``start2`` to ``stop2`` in + ``num_points2`` measuring param_meas at each step. + + Args: + param_set1: The QCoDeS parameter to sweep over in the outer loop + start1: Starting point of sweep in outer loop + stop1: End point of sweep in the outer loop + num_points1: Number of points to measure in the outer loop + delay1: Delay after setting parameter in the outer loop + param_set2: The QCoDeS parameter to sweep over in the inner loop + start2: Starting point of sweep in inner loop + stop2: End point of sweep in the inner loop + num_points2: Number of points to measure in the inner loop + delay2: Delay after setting parameter before measurement is performed + param_meas: Parameter(s) to measure at each step or functions that + will be called at each step. The function should take no arguments. + The parameters and functions are called in the order they are + supplied. + set_before_sweep: if True the outer parameter is set to its first value + before the inner parameter is swept to its next value. + enter_actions: A list of functions taking no arguments that will be + called before the measurements start + exit_actions: A list of functions taking no arguments that will be + called after the measurements ends + before_inner_actions: Actions executed before each run of the inner loop + after_inner_actions: Actions executed after each run of the inner loop + write_period: The time after which the data is actually written to the + database. + measurement_name: Name of the measurement. This will be passed down to + the dataset produced by the measurement. If not given, a default + value of 'results' is used for the dataset. + exp: The experiment to use for this measurement. + flush_columns: The data is written after a column is finished + independent of the passed time and write period. + additional_setpoints: A list of setpoint parameters to be registered in + the measurement but not scanned. + do_plot: should png and pdf versions of the images be saved after the + run. If None the setting will be read from ``qcodesrc.json`` + use_threads: If True measurements from each instrument will be done on + separate threads. If you are measuring from several instruments + this may give a significant speedup. + show_progress: should a progress bar be displayed during the + measurement. If None the setting will be read from ``qcodesrc.json`` + log_info: Message that is logged during the measurement. If None a default + message is used. + break_condition: Callable that takes no arguments. If returned True, + measurement is interrupted. + + Returns: + The QCoDeS dataset. + + """ + + if do_plot is None: + do_plot = cast("bool", config.dataset.dond_plot) + if show_progress is None: + show_progress = config.dataset.dond_show_progress + + meas = Measurement(name=measurement_name, exp=exp) + if log_info is not None: + meas._extra_log_info = log_info + else: + meas._extra_log_info = "Using 'qcodes.dataset.do2d'" + all_setpoint_params = ( + param_set1, + param_set2, + *tuple(s for s in additional_setpoints), + ) + + measured_parameters = tuple( + param for param in param_meas if isinstance(param, ParameterBase) + ) + + try: + loop_shape = (num_points1, num_points2, *tuple(1 for _ in additional_setpoints)) + shapes: Shapes | None = detect_shape_of_measurement( + measured_parameters, loop_shape + ) + except TypeError: + LOG.exception( + f"Could not detect shape of {measured_parameters} " + f"falling back to unknown shape." + ) + shapes = None + + _register_parameters(meas, all_setpoint_params) + _register_parameters(meas, param_meas, setpoints=all_setpoint_params, shapes=shapes) + _set_write_period(meas, write_period) + _register_actions(meas, enter_actions, exit_actions) + + if use_threads is None: + use_threads = config.dataset.use_threads + + param_meas_caller = ( + ThreadPoolParamsCaller(*param_meas) + if use_threads + else SequentialParamsCaller(*param_meas) + ) + + with ( + catch_interrupts() as interrupted, + meas.run() as datasaver, + param_meas_caller as call_param_meas, + ): + dataset = datasaver.dataset + additional_setpoints_data = process_params_meas(additional_setpoints) + setpoints1 = np.linspace(start1, stop1, num_points1) + for set_point1 in tqdm(setpoints1, disable=not show_progress): + if set_before_sweep: + param_set2.set(start2) + + param_set1.set(set_point1) + + for action in before_inner_actions: + action() + + time.sleep(delay1) + + setpoints2 = np.linspace(start2, stop2, num_points2) + + # flush to prevent unflushed print's to visually interrupt tqdm bar + # updates + sys.stdout.flush() + sys.stderr.flush() + for set_point2 in tqdm(setpoints2, disable=not show_progress, leave=False): + # skip first inner set point if `set_before_sweep` + if set_point2 == start2 and set_before_sweep: + pass + else: + param_set2.set(set_point2) + time.sleep(delay2) + + datasaver.add_result( + (param_set1, set_point1), + (param_set2, set_point2), + *call_param_meas(), + *additional_setpoints_data, + ) + + if callable(break_condition) and break_condition(): + raise BreakConditionInterrupt("Break condition was met.") + + for action in after_inner_actions: + action() + if flush_columns: + datasaver.flush_data_to_database() + + return _handle_plotting(dataset, do_plot, interrupted()) diff --git a/src/qcodes/dataset/_dond/do_nd.py b/src/qcodes/dataset/_dond/do_nd.py new file mode 100644 index 000000000000..7f8c5e56c45c --- /dev/null +++ b/src/qcodes/dataset/_dond/do_nd.py @@ -0,0 +1,889 @@ +from __future__ import annotations + +import itertools +import logging +import time +from collections.abc import Callable, Mapping, Sequence +from contextlib import ExitStack +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Literal, NotRequired, cast, overload + +import numpy as np +from opentelemetry import trace +from tqdm.auto import tqdm +from typing_extensions import TypedDict + +from qcodes import config +from qcodes.dataset._dond.do_nd_utils import ( + BreakConditionInterrupt, + _handle_plotting, + _register_actions, + _register_parameters, + _set_write_period, + catch_interrupts, +) +from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement +from qcodes.dataset.measurements import Measurement +from qcodes.dataset.threading import ( + SequentialParamsCaller, + ThreadPoolParamsCaller, + process_params_meas, +) +from qcodes.parameters import ParameterBase + +from .sweeps import AbstractSweep, TogetherSweep + +if TYPE_CHECKING: + from qcodes.dataset._dond.do_nd_utils import ( + ActionsT, + AxesTupleListWithDataSet, + BreakConditionT, + MultiAxesTupleListWithDataSet, + ParamMeasT, + ) + from qcodes.dataset.descriptions.versioning.rundescribertypes import Shapes + from qcodes.dataset.experiment_container import Experiment + +LOG = logging.getLogger(__name__) +SweepVarType = Any + +TRACER = trace.get_tracer(__name__) + + +class ParameterGroup(TypedDict): + params: tuple[ParamMeasT, ...] + meas_name: str + + +@dataclass +class ParameterSetEvent: + parameter: ParameterBase + new_value: SweepVarType + should_set: bool + delay: float + actions: ActionsT + get_after_set: bool + + +class _Sweeper: + def __init__( + self, + sweeps: Sequence[AbstractSweep | TogetherSweep], + additional_setpoints: Sequence[ParameterBase], + ): + self._additional_setpoints = additional_setpoints + self._sweeps = sweeps + self._setpoints = self._make_setpoints_tuples() + self._setpoints_dict = self._make_setpoints_dict() + self._shape = self._make_shape(sweeps, additional_setpoints) + self._iter_index = 0 + + @property + def setpoints_dict(self) -> dict[str, list[Any]]: + return self._setpoints_dict + + def _make_setpoints_tuples( + self, + ) -> tuple[tuple[tuple[SweepVarType, ...] | SweepVarType, ...], ...]: + sweeps = tuple(sweep.get_setpoints() for sweep in self._sweeps) + return cast( + "tuple[tuple[tuple[SweepVarType, ...] | SweepVarType, ...], ...]", + tuple(itertools.product(*sweeps)), + ) + + def _make_single_point_setpoints_dict(self, index: int) -> dict[str, SweepVarType]: + setpoint_dict = {} + values = self._setpoints[index] + for sweep, subvalues in zip(self._sweeps, values): + if isinstance(sweep, TogetherSweep): + for individual_sweep, value in zip(sweep.sweeps, subvalues): + setpoint_dict[individual_sweep.param.full_name] = value + else: + setpoint_dict[sweep.param.full_name] = subvalues + return setpoint_dict + + def _make_setpoints_dict(self) -> dict[str, list[Any]]: + setpoint_dict: dict[str, list[SweepVarType]] = {} + + for sweep in self._sweeps: + if isinstance(sweep, TogetherSweep): + for individual_sweep in sweep.sweeps: + setpoint_dict[individual_sweep.param.full_name] = [] + else: + setpoint_dict[sweep.param.full_name] = [] + + for setpoint_tuples in self._setpoints: + for sweep, values in zip(self._sweeps, setpoint_tuples): + if isinstance(sweep, TogetherSweep): + for individual_sweep, individual_value in zip(sweep.sweeps, values): + setpoint_dict[individual_sweep.param.full_name].append( + individual_value + ) + else: + setpoint_dict[sweep.param.full_name].append(values) + return setpoint_dict + + @property + def all_sweeps(self) -> tuple[AbstractSweep, ...]: + sweeps: list[AbstractSweep] = [] + for sweep in self._sweeps: + if isinstance(sweep, TogetherSweep): + sweeps.extend(sweep.sweeps) + else: + sweeps.append(sweep) + return tuple(sweeps) + + @property + def all_setpoint_params(self) -> tuple[ParameterBase, ...]: + return tuple(sweep.param for sweep in self.all_sweeps) + tuple( + self._additional_setpoints + ) + + @property + def sweep_groupes(self) -> tuple[tuple[ParameterBase, ...], ...]: + """ + These are all the combinations of setpoints we consider + valid for setpoints in a dataset. A dataset must depend on + at least one parameter from each dimension of the dond. + For dimensions that uses TogetherSweep they may depend + on one or more of the parameters of that sweep. + """ + param_tuple_list: list[tuple[ParameterBase, ...]] = [] + for sweep in self._sweeps: + if isinstance(sweep, TogetherSweep): + param_tuple_list.append( + tuple(sub_sweep.param for sub_sweep in sweep.sweeps) + ) + else: + param_tuple_list.append((sweep.param,)) + + param_tuple_list.extend( + [(setpoint,) for setpoint in self._additional_setpoints] + ) + + # in param_tuple_list there is a tuple of possible setpoints for each + # dim in the dond. For regular sweeps this is a 1 tuple but for + # a TogetherSweep it is of length of the number of parameters. + + # now we expand to a list of setpoints in a TogetherSweep + # to a list of all possible combinations of these. + + expanded_param_tuples = tuple( + tuple( + itertools.chain.from_iterable( + itertools.combinations(param_tuple, j + 1) + for j in range(len(param_tuple)) + ) + ) + for param_tuple in param_tuple_list + ) + + # next we generate all valid combinations of picking one parameter from each + # dimension in the setpoints. + setpoint_combinations = itertools.product(*expanded_param_tuples) + + setpoint_combinations_expanded = tuple( + tuple(itertools.chain.from_iterable(setpoint_combination)) + for setpoint_combination in setpoint_combinations + ) + + return setpoint_combinations_expanded + + @staticmethod + def _make_shape( + sweeps: Sequence[AbstractSweep | TogetherSweep], + addtional_setpoints: Sequence[ParameterBase], + ) -> tuple[int, ...]: + loop_shape = tuple(sweep.num_points for sweep in sweeps) + tuple( + 1 for _ in addtional_setpoints + ) + return loop_shape + + @property + def shape(self) -> tuple[int, ...]: + return self._shape + + def __getitem__(self, index: int) -> tuple[ParameterSetEvent, ...]: + setpoints = self._make_single_point_setpoints_dict(index) + + if index == 0: + previous_setpoints: dict[str, SweepVarType | None] = {} + for key in setpoints: + previous_setpoints[key] = None + else: + previous_setpoints = self._make_single_point_setpoints_dict(index - 1) + + parameter_set_events = [] + + for sweep in self.all_sweeps: + new_value = setpoints[sweep.param.full_name] + old_value = previous_setpoints[sweep.param.full_name] + if old_value is None or old_value != new_value: + should_set = True + else: + should_set = False + event = ParameterSetEvent( + new_value=new_value, + parameter=sweep.param, + should_set=should_set, + delay=sweep.delay, + actions=sweep.post_actions, + get_after_set=sweep.get_after_set, + ) + parameter_set_events.append(event) + return tuple(parameter_set_events) + + def __len__(self) -> int: + return int(np.prod(self.shape)) + + def __iter__(self) -> _Sweeper: + return self + + def __next__(self) -> tuple[ParameterSetEvent, ...]: + if self._iter_index < len(self): + return_val = self[self._iter_index] + self._iter_index += 1 + return return_val + else: + raise StopIteration + + +class _Measurements: + def __init__( + self, + sweeper: _Sweeper, + measurement_name: str | Sequence[str], + params_meas: Sequence[ParamMeasT | Sequence[ParamMeasT]], + enter_actions: ActionsT, + exit_actions: ActionsT, + experiments: Experiment | Sequence[Experiment] | None, + write_period: float | None, + log_info: str | None, + dataset_dependencies: Mapping[str, Sequence[ParamMeasT]] | None = None, + ): + self._sweeper = sweeper + self._enter_actions = enter_actions + self._exit_actions = exit_actions + self._write_period = write_period + self._log_info = log_info + if log_info is not None: + self._extra_log_info = log_info + else: + self._extra_log_info = "Using 'qcodes.dataset.dond'" + + ( + self._measured_all, + grouped_parameters, + self._measured_parameters, + ) = self._extract_parameters_by_type_and_group(params_meas) + + self._shapes = self._get_shapes() + + if dataset_dependencies and len(grouped_parameters) > 1: + raise ValueError( + "Measured parameters have been grouped both in input " + "and given in dataset dependencies. This is not supported, " + "group measurement parameters either in input or in " + "dataset dependencies." + ) + + if dataset_dependencies is None: + self._groups = self._create_groups_from_grouped_parameters( + grouped_parameters, experiments, measurement_name + ) + elif dataset_dependencies: + _validate_dataset_dependencies_and_names( + dataset_dependencies, measurement_name + ) + dataset_dependencies_split = self._split_dateset_dependencies( + dataset_dependencies + ) + self._groups = self._create_groups_from_dataset_dependencies( + dataset_dependencies_split, + self._measured_parameters, + experiments, + measurement_name, + ) + + @property + def measured_all(self) -> tuple[ParamMeasT, ...]: + return self._measured_all + + @property + def measured_parameters(self) -> tuple[ParameterBase, ...]: + return self._measured_parameters + + @property + def shapes(self) -> Shapes | None: + return self._shapes + + @property + def groups(self) -> tuple[_SweepMeasGroup, ...]: + return self._groups + + @staticmethod + def _create_measurement_names( + measurement_name: str | Sequence[str], n_names_required: int + ) -> tuple[str, ...]: + if isinstance(measurement_name, str): + return (measurement_name,) * n_names_required + else: + if len(measurement_name) != n_names_required: + raise ValueError( + f"Got {len(measurement_name)} measurement names " + f"but should create {n_names_required} dataset(s)." + ) + return tuple(measurement_name) + + @staticmethod + def _extract_parameters_by_type_and_group( + params_meas: Sequence[ParamMeasT | Sequence[ParamMeasT]], + ) -> tuple[ + tuple[ParamMeasT, ...], + tuple[tuple[ParamMeasT, ...], ...], + tuple[ParameterBase, ...], + ]: + measured_parameters: list[ParameterBase] = [] + measured_all: list[ParamMeasT] = [] + single_group: list[ParamMeasT] = [] + multi_group: list[tuple[ParamMeasT, ...]] = [] + grouped_parameters: tuple[tuple[ParamMeasT, ...], ...] + for param in params_meas: + if not isinstance(param, Sequence): + single_group.append(param) + measured_all.append(param) + if isinstance(param, ParameterBase): + measured_parameters.append(param) + elif not isinstance(param, str): + multi_group.append(tuple(param)) + for nested_param in param: + measured_all.append(nested_param) + if isinstance(nested_param, ParameterBase): + measured_parameters.append(nested_param) + + if single_group and multi_group: + raise ValueError( + f"Got both grouped and non grouped " + f"parameters to measure in " + f"{params_meas}. This is not supported." + ) + + if single_group: + grouped_parameters = (tuple(single_group),) + elif multi_group: + grouped_parameters = tuple(multi_group) + else: + raise ValueError("No parameters to measure supplied") + + return tuple(measured_all), grouped_parameters, tuple(measured_parameters) + + def _get_shapes(self) -> Shapes | None: + try: + shapes: Shapes | None = detect_shape_of_measurement( + self.measured_parameters, self._sweeper.shape + ) + LOG.debug("Detected shapes to be %s", shapes) + except TypeError: + LOG.exception( + f"Could not detect shape of {self.measured_parameters} " + f"falling back to unknown shape." + ) + shapes = None + return shapes + + @staticmethod + def _get_experiments( + experiments: Experiment | Sequence[Experiment] | None, + n_experiments_required: int, + ) -> Sequence[Experiment | None]: + if not isinstance(experiments, Sequence): + experiments_internal: Sequence[Experiment | None] = [ + experiments + ] * n_experiments_required + else: + experiments_internal = experiments + + if len(experiments_internal) != n_experiments_required: + raise ValueError( + f"Inconsistent number of " + f"datasets and experiments, " + f"got {n_experiments_required} and {len(experiments_internal)}." + ) + return experiments_internal + + def _create_groups_from_grouped_parameters( + self, + grouped_parameters: tuple[tuple[ParamMeasT, ...], ...], + experiments: Experiment | Sequence[Experiment] | None, + meas_names: str | Sequence[str], + ) -> tuple[_SweepMeasGroup, ...]: + setpoints = self._sweeper.all_setpoint_params + + groups = [] + m_group: Sequence[ParamMeasT] + + experiments_internal = self._get_experiments( + experiments, len(grouped_parameters) + ) + measurement_names = self._create_measurement_names( + meas_names, len(grouped_parameters) + ) + + for m_group, experiment, meas_name in zip( + grouped_parameters, + experiments_internal, + measurement_names, + ): + meas_ctx = self._create_measurement_ctx_manager( + experiment, meas_name, setpoints, tuple(m_group) + ) + s_m_group = _SweepMeasGroup(setpoints, tuple(m_group), meas_ctx) + groups.append(s_m_group) + return tuple(groups) + + def _create_groups_from_dataset_dependencies( + self, + dataset_dependencies: dict[ + str, tuple[Sequence[ParameterBase], Sequence[ParamMeasT]] + ], + all_measured_parameters: tuple[ParameterBase, ...], + experiments: Experiment | Sequence[Experiment] | None, + meas_names: str | Sequence[str], + ) -> tuple[_SweepMeasGroup, ...]: + potential_setpoint_groups = self._sweeper.sweep_groupes + + experiments_internal = self._get_experiments( + experiments, len(dataset_dependencies) + ) + if meas_names == "": + meas_names = tuple(dataset_dependencies.keys()) + + measurement_names = self._create_measurement_names( + meas_names, len(dataset_dependencies) + ) + + all_dataset_dependencies_meas_parameters = tuple( + itertools.chain.from_iterable( + output[1] for output in dataset_dependencies.values() + ) + ) + + for meas_param in all_measured_parameters: + if meas_param not in all_dataset_dependencies_meas_parameters: + raise ValueError( + f"Parameter {meas_param} is measured but not added " + f"to any dataset in dataset_dependencies." + ) + + groups = [] + for experiment, meas_name in zip( + experiments_internal, + measurement_names, + ): + (sp_group, m_group) = dataset_dependencies[meas_name] + if tuple(sp_group) not in potential_setpoint_groups: + raise ValueError( + f"dataset_dependencies contains {sp_group} " + f"which is not among the expected groups of setpoints " + f"{potential_setpoint_groups}" + ) + + LOG.info( + f"creating context manager for setpoints" + f" {sp_group} and measurement parameters {m_group}" + ) + meas_ctx = self._create_measurement_ctx_manager( + experiment, meas_name, tuple(sp_group), tuple(m_group) + ) + s_m_group = _SweepMeasGroup(tuple(sp_group), tuple(m_group), meas_ctx) + groups.append(s_m_group) + return tuple(groups) + + def _create_measurement_ctx_manager( + self, + experiment: Experiment | None, + measurement_name: str, + sweep_parameters: Sequence[ParameterBase], + measure_parameters: Sequence[ParamMeasT], + ) -> Measurement: + meas = Measurement(name=measurement_name, exp=experiment) + _register_parameters(meas, sweep_parameters) + _register_parameters( + meas, + measure_parameters, + setpoints=sweep_parameters, + shapes=self.shapes, + ) + meas._extra_log_info = self._extra_log_info + _set_write_period(meas, self._write_period) + _register_actions(meas, self._enter_actions, self._exit_actions) + return meas + + def _split_dateset_dependencies( + self, + dataset_dependencies: Mapping[str, Sequence[ParamMeasT]], + ) -> dict[str, tuple[Sequence[ParameterBase], Sequence[ParamMeasT]]]: + # split measured parameters from setpoint parameters using param_meas + dataset_dependencies_split: dict[ + str, tuple[Sequence[ParameterBase], Sequence[ParamMeasT]] + ] = {} + for name, dataset_parameters in dataset_dependencies.items(): + meas_parameters = tuple( + param for param in dataset_parameters if param in self.measured_all + ) + setpoint_parameters = cast( + "Sequence[ParameterBase]", + tuple( + param + for param in dataset_parameters + if param not in self.measured_all + ), + ) + dataset_dependencies_split[name] = ( + setpoint_parameters, + meas_parameters, + ) + return dataset_dependencies_split + + +# idealy we would want this to be frozen but then postinit +# cannot calculate all the parameters +# https://stackoverflow.com/questions/53756788/ +@dataclass(frozen=False) +class _SweepMeasGroup: + sweep_parameters: tuple[ParameterBase, ...] + measure_parameters: tuple[ParamMeasT, ...] + measurement_cxt: Measurement + + def __post_init__(self) -> None: + meas_parameters = tuple( + a for a in self.measure_parameters if isinstance(a, ParameterBase) + ) + self._parameters = self.sweep_parameters + meas_parameters + + @property + def parameters(self) -> tuple[ParameterBase, ...]: + return self._parameters + + +class DondKWargs(TypedDict): + write_period: NotRequired[float | None] + measurement_name: NotRequired[str | Sequence[str]] + exp: NotRequired[Experiment | Sequence[Experiment] | None] + enter_actions: NotRequired[ActionsT] + exit_actions: NotRequired[ActionsT] + do_plot: NotRequired[bool | None] + show_progress: NotRequired[bool | None] + use_threads: NotRequired[bool | None] + additional_setpoints: NotRequired[Sequence[ParameterBase]] + log_info: NotRequired[str | None] + break_condition: NotRequired[BreakConditionT | None] + dataset_dependencies: NotRequired[Mapping[str, Sequence[ParamMeasT]]] + in_memory_cache: NotRequired[bool | None] + + +@overload +def dond( + *params: AbstractSweep | TogetherSweep | ParamMeasT | Sequence[ParamMeasT], + write_period: float | None = None, + measurement_name: str | Sequence[str] = "", + exp: Experiment | Sequence[Experiment] | None = None, + enter_actions: ActionsT = (), + exit_actions: ActionsT = (), + do_plot: bool | None = None, + show_progress: bool | None = None, + use_threads: bool | None = None, + additional_setpoints: Sequence[ParameterBase] = (), + log_info: str | None = None, + break_condition: BreakConditionT | None = None, + dataset_dependencies: Mapping[str, Sequence[ParamMeasT]] | None = None, + in_memory_cache: bool | None = None, + squeeze: Literal[False], +) -> MultiAxesTupleListWithDataSet: ... + + +@overload +def dond( + *params: AbstractSweep | TogetherSweep | ParamMeasT | Sequence[ParamMeasT], + write_period: float | None = None, + measurement_name: str | Sequence[str] = "", + exp: Experiment | Sequence[Experiment] | None = None, + enter_actions: ActionsT = (), + exit_actions: ActionsT = (), + do_plot: bool | None = None, + show_progress: bool | None = None, + use_threads: bool | None = None, + additional_setpoints: Sequence[ParameterBase] = (), + log_info: str | None = None, + break_condition: BreakConditionT | None = None, + dataset_dependencies: Mapping[str, Sequence[ParamMeasT]] | None = None, + in_memory_cache: bool | None = None, + squeeze: Literal[True], +) -> AxesTupleListWithDataSet | MultiAxesTupleListWithDataSet: ... + + +@overload +def dond( + *params: AbstractSweep | TogetherSweep | ParamMeasT | Sequence[ParamMeasT], + write_period: float | None = None, + measurement_name: str | Sequence[str] = "", + exp: Experiment | Sequence[Experiment] | None = None, + enter_actions: ActionsT = (), + exit_actions: ActionsT = (), + do_plot: bool | None = None, + show_progress: bool | None = None, + use_threads: bool | None = None, + additional_setpoints: Sequence[ParameterBase] = (), + log_info: str | None = None, + break_condition: BreakConditionT | None = None, + dataset_dependencies: Mapping[str, Sequence[ParamMeasT]] | None = None, + in_memory_cache: bool | None = None, + squeeze: bool = True, +) -> AxesTupleListWithDataSet | MultiAxesTupleListWithDataSet: ... + + +@TRACER.start_as_current_span("qcodes.dataset.dond") +def dond( + *params: AbstractSweep | TogetherSweep | ParamMeasT | Sequence[ParamMeasT], + write_period: float | None = None, + measurement_name: str | Sequence[str] = "", + exp: Experiment | Sequence[Experiment] | None = None, + enter_actions: ActionsT = (), + exit_actions: ActionsT = (), + do_plot: bool | None = None, + show_progress: bool | None = None, + use_threads: bool | None = None, + additional_setpoints: Sequence[ParameterBase] = (), + log_info: str | None = None, + break_condition: BreakConditionT | None = None, + dataset_dependencies: Mapping[str, Sequence[ParamMeasT]] | None = None, + in_memory_cache: bool | None = None, + squeeze: bool = True, +) -> AxesTupleListWithDataSet | MultiAxesTupleListWithDataSet: + """ + Perform n-dimentional scan from slowest (first) to the fastest (last), to + measure m measurement parameters. The dimensions should be specified + as sweep objects, and after them the parameters to measure should be passed. + + Args: + params: Instances of n sweep classes and m measurement parameters, + e.g. if linear sweep is considered: + + .. code-block:: + + LinSweep(param_set_1, start_1, stop_1, num_points_1, delay_1), ..., + LinSweep(param_set_n, start_n, stop_n, num_points_n, delay_n), + param_meas_1, param_meas_2, ..., param_meas_m + + If multiple DataSets creation is needed, measurement parameters should + be grouped, so one dataset will be created for each group. e.g.: + + .. code-block:: + + LinSweep(param_set_1, start_1, stop_1, num_points_1, delay_1), ..., + LinSweep(param_set_n, start_n, stop_n, num_points_n, delay_n), + [param_meas_1, param_meas_2], ..., [param_meas_m] + + If you want to sweep multiple parameters together. + + .. code-block:: + + TogetherSweep(LinSweep(param_set_1, start_1, stop_1, num_points, delay_1), + LinSweep(param_set_2, start_2, stop_2, num_points, delay_2)) + param_meas_1, param_meas_2, ..., param_meas_m + + + write_period: The time after which the data is actually written to the + database. + measurement_name: Name(s) of the measurement. This will be passed down to + the dataset produced by the measurement. If not given, a default + value of 'results' is used for the dataset. If more than one is + given, each dataset will have an individual name. + exp: The experiment to use for this measurement. If you create multiple + measurements using groups you may also supply multiple experiments. + enter_actions: A list of functions taking no arguments that will be + called before the measurements start. + exit_actions: A list of functions taking no arguments that will be + called after the measurements ends. + do_plot: should png and pdf versions of the images be saved and plots + are shown after the run. If None the setting will be read from + ``qcodesrc.json`` + show_progress: should a progress bar be displayed during the + measurement. If None the setting will be read from ``qcodesrc.json`` + use_threads: If True, measurements from each instrument will be done on + separate threads. If you are measuring from several instruments + this may give a significant speedup. + additional_setpoints: A list of setpoint parameters to be registered in + the measurement but not scanned/swept-over. + log_info: Message that is logged during the measurement. If None a default + message is used. + break_condition: Callable that takes no arguments. If returned True, + measurement is interrupted. + dataset_dependencies: Optionally describe that measured datasets only depend + on a subset of the setpoint parameters. Given as a mapping from + measurement names to Sequence of Parameters. Note that a dataset must + depend on at least one parameter from each dimension but can depend + on one or more parameters from a dimension sweeped with a TogetherSweep. + in_memory_cache: + Should a cache of the data be kept available in memory for faster + plotting and exporting. Useful to disable if the data is very large + in order to save on memory consumption. + If ``None``, the value for this will be read from ``qcodesrc.json`` config file. + squeeze: If True, will return a tuple of QCoDeS DataSet, Matplotlib axis, + Matplotlib colorbar if only one group of measurements was performed + and a tuple of tuples of these if more than one group of measurements + was performed. If False, will always return a tuple where the first + member is a tuple of QCoDeS DataSet(s) and the second member is a tuple + of Matplotlib axis(es) and the third member is a tuple of Matplotlib + colorbar(s). + + Returns: + A tuple of QCoDeS DataSet, Matplotlib axis, Matplotlib colorbar. If + more than one group of measurement parameters is supplied, the output + will be a tuple of tuple(QCoDeS DataSet), tuple(Matplotlib axis), + tuple(Matplotlib colorbar), in which each element of each sub-tuple + belongs to one group, and the order of elements is the order of + the supplied groups. + + """ + if do_plot is None: + do_plot = cast("bool", config.dataset.dond_plot) + if show_progress is None: + show_progress = config.dataset.dond_show_progress + + sweep_instances, params_meas = _parse_dond_arguments(*params) + + sweeper = _Sweeper(sweep_instances, additional_setpoints) + + measurements = _Measurements( + sweeper, + measurement_name, + params_meas, + enter_actions, + exit_actions, + exp, + write_period, + log_info, + dataset_dependencies, + ) + + LOG.info( + "Starting a doNd with scan with\n setpoints: %s,\n measuring: %s", + sweeper.all_setpoint_params, + measurements.measured_all, + ) + LOG.debug( + "dond has been grouped into the following datasets:\n%s", + measurements.groups, + ) + + datasets = [] + plots_axes = [] + plots_colorbar = [] + if use_threads is None: + use_threads = config.dataset.use_threads + + params_meas_caller = ( + ThreadPoolParamsCaller(*measurements.measured_all) + if use_threads + else SequentialParamsCaller(*measurements.measured_all) + ) + + datasavers = [] + interrupted: Callable[ # noqa: E731 + [], KeyboardInterrupt | BreakConditionInterrupt | None + ] = lambda: None + try: + with ( + catch_interrupts() as interrupted, + ExitStack() as stack, + params_meas_caller as call_params_meas, + ): + datasavers = [ + stack.enter_context( + group.measurement_cxt.run(in_memory_cache=in_memory_cache) + ) + for group in measurements.groups + ] + additional_setpoints_data = process_params_meas(additional_setpoints) + for set_events in tqdm(sweeper, disable=not show_progress): + LOG.debug("Processing set events: %s", set_events) + results: dict[ParameterBase, Any] = {} + for set_event in set_events: + if set_event.should_set: + set_event.parameter(set_event.new_value) + for act in set_event.actions: + act() + time.sleep(set_event.delay) + if set_event.get_after_set: + set_event.new_value = set_event.parameter() + + results[set_event.parameter] = set_event.new_value + + meas_value_pair = call_params_meas() + for meas_param, value in meas_value_pair: + results[meas_param] = value + + for datasaver, group in zip(datasavers, measurements.groups): + filtered_results_list = [ + (param, value) + for param, value in results.items() + if param in group.parameters + ] + datasaver.add_result( + *filtered_results_list, + *additional_setpoints_data, + ) + + if callable(break_condition) and break_condition(): + raise BreakConditionInterrupt("Break condition was met.") + finally: + for datasaver in datasavers: + ds, plot_axis, plot_color = _handle_plotting( + datasaver.dataset, do_plot, interrupted() + ) + datasets.append(ds) + plots_axes.append(plot_axis) + plots_colorbar.append(plot_color) + + if len(measurements.groups) == 1 and squeeze is True: + return datasets[0], plots_axes[0], plots_colorbar[0] + else: + return tuple(datasets), tuple(plots_axes), tuple(plots_colorbar) + + +def _validate_dataset_dependencies_and_names( + dataset_dependencies: Mapping[str, Sequence[ParamMeasT]] | None, + measurement_name: str | Sequence[str], +) -> None: + if dataset_dependencies is not None and measurement_name != "": + if isinstance(measurement_name, str): + raise ValueError( + "Creating multiple datasets but only one measurement name given." + ) + if set(dataset_dependencies.keys()) != set(measurement_name): + raise ValueError( + f"Inconsistent measurement names: measurement_name " + f"contains {measurement_name} " + f"but dataset_dependencies contains " + f"{tuple(dataset_dependencies.keys())}." + ) + + +def _parse_dond_arguments( + *params: AbstractSweep | TogetherSweep | ParamMeasT | Sequence[ParamMeasT], +) -> tuple[ + list[AbstractSweep | TogetherSweep], list[ParamMeasT | Sequence[ParamMeasT]] +]: + """ + Parse supplied arguments into sweep objects and measurement parameters + and their callables. + """ + sweep_instances: list[AbstractSweep | TogetherSweep] = [] + params_meas: list[ParamMeasT | Sequence[ParamMeasT]] = [] + for par in params: + if isinstance(par, (AbstractSweep, TogetherSweep)): + sweep_instances.append(par) + else: + params_meas.append(par) + return sweep_instances, params_meas diff --git a/src/qcodes/dataset/_dond/do_nd_utils.py b/src/qcodes/dataset/_dond/do_nd_utils.py new file mode 100644 index 000000000000..5d0339c23b43 --- /dev/null +++ b/src/qcodes/dataset/_dond/do_nd_utils.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +import logging +from collections.abc import Callable, Generator, Sequence +from contextlib import contextmanager +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import matplotlib.axes + import matplotlib.colorbar + + from qcodes.dataset.measurements import Measurement + +from qcodes.dataset.data_set_protocol import DataSetProtocol +from qcodes.dataset.plotting import plot_and_save_image +from qcodes.parameters import MultiParameter, ParameterBase + +if TYPE_CHECKING: + from qcodes.dataset.descriptions.versioning.rundescribertypes import Shapes + +log = logging.getLogger(__name__) + +ActionsT = Sequence[Callable[[], None]] +BreakConditionT = Callable[[], bool] + +ParamMeasT = ParameterBase | Callable[[], None] + +AxesTuple = tuple["matplotlib.axes.Axes", "matplotlib.colorbar.Colorbar"] +AxesTupleList = tuple[ + list["matplotlib.axes.Axes"], list["matplotlib.colorbar.Colorbar | None"] +] +AxesTupleListWithDataSet = tuple[ + DataSetProtocol, + tuple["matplotlib.axes.Axes | None", ...], + tuple["matplotlib.colorbar.Colorbar | None", ...], +] +MultiAxesTupleListWithDataSet = tuple[ + tuple[DataSetProtocol, ...], + tuple[tuple["matplotlib.axes.Axes | None", ...], ...], + tuple[tuple["matplotlib.colorbar.Colorbar | None", ...], ...], +] + + +class BreakConditionInterrupt(Exception): + pass + + +MeasInterruptT = KeyboardInterrupt | BreakConditionInterrupt | None + + +def _register_parameters( + meas: Measurement, + param_meas: Sequence[ParamMeasT], + setpoints: Sequence[ParameterBase] | None = None, + shapes: Shapes | None = None, +) -> None: + real_parameters = [ + param for param in param_meas if isinstance(param, ParameterBase) + ] + + for parameter in real_parameters: + meas.register_parameter(parameter, setpoints=setpoints) + + if shapes is not None: + parameter_names = [param.register_name for param in real_parameters] + for param in real_parameters: + if isinstance(param, MultiParameter): + parameter_names.extend(param.full_names) + + filtered_shapes = { + name: shape for name, shape in shapes.items() if name in parameter_names + } + meas.set_shapes(shapes=filtered_shapes) + + +def _set_write_period(meas: Measurement, write_period: float | None = None) -> None: + if write_period is not None: + meas.write_period = write_period + + +def _handle_plotting( + data: DataSetProtocol, + do_plot: bool = True, + interrupted: MeasInterruptT = None, +) -> AxesTupleListWithDataSet: + """ + Save the plots created by datasaver as pdf and png + + Args: + data: a dataset to generate plots from + as plot. + do_plot: Should a plot be produced + interrupted: If the measurement was interrupted, this will be the exception. + + """ + res: AxesTupleListWithDataSet + if do_plot: + res = plot_and_save_image(data) + else: + res = data, (None,), (None,) + + if interrupted: + log.warning( + f"Measurement has been interrupted, data may be incomplete: {interrupted}" + ) + + return res + + +def _register_actions( + meas: Measurement, enter_actions: ActionsT, exit_actions: ActionsT +) -> None: + for action in enter_actions: + # this omits the possibility of passing + # argument to enter and exit actions. + # Do we want that? + meas.add_before_run(action, ()) + for action in exit_actions: + meas.add_after_run(action, ()) + + +@contextmanager +def catch_interrupts() -> Generator[Callable[[], MeasInterruptT | None], None, None]: + interrupt_exception: MeasInterruptT | None = None + interrupt_raised = False + + def get_interrupt_exception() -> MeasInterruptT | None: + nonlocal interrupt_exception + return interrupt_exception + + try: + yield get_interrupt_exception + except KeyboardInterrupt as e: + interrupt_exception = e + interrupt_raised = True + raise # Re-raise KeyboardInterrupt + except BreakConditionInterrupt as e: + interrupt_exception = e + interrupt_raised = True + # Don't re-raise BreakConditionInterrupt + finally: + if interrupt_raised: + log.warning( + f"Measurement has been interrupted, data may be incomplete: {interrupt_exception}" + ) diff --git a/src/qcodes/dataset/_dond/sweeps.py b/src/qcodes/dataset/_dond/sweeps.py new file mode 100644 index 000000000000..fb333af48daa --- /dev/null +++ b/src/qcodes/dataset/_dond/sweeps.py @@ -0,0 +1,276 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +import numpy as np +import numpy.typing as npt + +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + + from qcodes.dataset._dond.do_nd_utils import ActionsT + from qcodes.parameters import ParameterBase + + +class AbstractSweep[T: np.generic](ABC): + """ + Abstract sweep class that defines an interface for concrete sweep classes. + """ + + @abstractmethod + def get_setpoints(self) -> npt.NDArray[T]: + """ + Returns an array of setpoint values for this sweep. + """ + + @property + @abstractmethod + def param(self) -> ParameterBase: + """ + Returns the Qcodes sweep parameter. + """ + + @property + @abstractmethod + def delay(self) -> float: + """ + Delay between two consecutive sweep points. + """ + + @property + @abstractmethod + def num_points(self) -> int: + """ + Number of sweep points. + """ + + @property + @abstractmethod + def post_actions(self) -> ActionsT: + """ + Actions to be performed after setting param to its setpoint. + """ + + @property + def get_after_set(self) -> bool: + """ + Should we perform a call to get on the parameter after setting it + and store that rather than the setpoint value in the dataset? + + This defaults to False for backwards compatibility + but subclasses should overwrite this to implement if correctly. + """ + return False + + +class LinSweep(AbstractSweep[np.floating]): + """ + Linear sweep. + + Args: + param: Qcodes parameter to sweep. + start: Sweep start value. + stop: Sweep end value. + num_points: Number of sweep points. + delay: Time in seconds between two consecutive sweep points. + post_actions: Actions to do after each sweep point. + get_after_set: Should we perform a get on the parameter after setting it + and store the value returned by get rather than the set value in the dataset. + + """ + + def __init__( + self, + param: ParameterBase, + start: float, + stop: float, + num_points: int, + delay: float = 0, + post_actions: ActionsT = (), + get_after_set: bool = False, + ): + self._param = param + self._start = start + self._stop = stop + self._num_points = num_points + self._delay = delay + self._post_actions = post_actions + self._get_after_set = get_after_set + + def get_setpoints(self) -> npt.NDArray[np.floating]: + """ + Linear (evenly spaced) numpy array for supplied start, stop and + num_points. + """ + return np.linspace(self._start, self._stop, self._num_points) + + @property + def param(self) -> ParameterBase: + return self._param + + @property + def delay(self) -> float: + return self._delay + + @property + def num_points(self) -> int: + return self._num_points + + @property + def post_actions(self) -> ActionsT: + return self._post_actions + + @property + def get_after_set(self) -> bool: + return self._get_after_set + + +class LogSweep(AbstractSweep[np.floating]): + """ + Logarithmic sweep. + + Args: + param: Qcodes parameter for sweep. + start: Sweep start value. + stop: Sweep end value. + num_points: Number of sweep points. + delay: Time in seconds between two consecutive sweep points. + post_actions: Actions to do after each sweep point. + get_after_set: Should we perform a get on the parameter after setting it + and store the value returned by get rather than the set value in the dataset. + + """ + + def __init__( + self, + param: ParameterBase, + start: float, + stop: float, + num_points: int, + delay: float = 0, + post_actions: ActionsT = (), + get_after_set: bool = False, + ): + self._param = param + self._start = start + self._stop = stop + self._num_points = num_points + self._delay = delay + self._post_actions = post_actions + self._get_after_set = get_after_set + + def get_setpoints(self) -> npt.NDArray[np.floating]: + """ + Logarithmically spaced numpy array for supplied start, stop and + num_points. + """ + return np.logspace(self._start, self._stop, self._num_points) + + @property + def param(self) -> ParameterBase: + return self._param + + @property + def delay(self) -> float: + return self._delay + + @property + def num_points(self) -> int: + return self._num_points + + @property + def post_actions(self) -> ActionsT: + return self._post_actions + + @property + def get_after_set(self) -> bool: + return self._get_after_set + + +class ArraySweep[T: np.generic](AbstractSweep): + """ + Sweep the values of a given array. + + Args: + param: Qcodes parameter for sweep. + array: array with values to sweep. + delay: Time in seconds between two consecutive sweep points. + post_actions: Actions to do after each sweep point. + get_after_set: Should we perform a get on the parameter after setting it + and store the value returned by get rather than the set value in the dataset. + + """ + + def __init__( + self, + param: ParameterBase, + array: Sequence[Any] | npt.NDArray[T], + delay: float = 0, + post_actions: ActionsT = (), + get_after_set: bool = False, + ): + self._param = param + self._array = np.array(array) + self._delay = delay + self._post_actions = post_actions + self._get_after_set = get_after_set + + def get_setpoints(self) -> npt.NDArray[T]: + return self._array + + @property + def param(self) -> ParameterBase: + return self._param + + @property + def delay(self) -> float: + return self._delay + + @property + def num_points(self) -> int: + return len(self._array) + + @property + def post_actions(self) -> ActionsT: + return self._post_actions + + @property + def get_after_set(self) -> bool: + return self._get_after_set + + +class TogetherSweep: + """ + A combination of Multiple sweeps that are to be performed in parallel + such that all parameters in the `TogetherSweep` are set to the next value + before a parameter is read. + + """ + + def __init__(self, *sweeps: AbstractSweep): + if len(sweeps) == 0: + raise ValueError("A TogetherSweep must contain at least one sweep.") + + len_0 = sweeps[0].num_points + + for sweep in sweeps: + if sweep.num_points != len_0: + raise ValueError( + f"All Sweeps in a TogetherSweep must have the same length." + f"Sweep of {sweep.param} had {sweep.num_points} but the " + f"first one had {len_0}." + ) + + self._sweeps = tuple(sweeps) + + @property + def sweeps(self) -> tuple[AbstractSweep, ...]: + return self._sweeps + + def get_setpoints(self) -> Iterable: + return zip(*(sweep.get_setpoints() for sweep in self.sweeps)) + + @property + def num_points(self) -> int: + return self.sweeps[0].num_points diff --git a/src/qcodes/dataset/dond/__init__.py b/src/qcodes/dataset/dond/__init__.py index e69de29bb2d1..9c09ec731ed7 100644 --- a/src/qcodes/dataset/dond/__init__.py +++ b/src/qcodes/dataset/dond/__init__.py @@ -0,0 +1,3 @@ +from qcodes.utils.deprecate import _warn_deprecated_module + +_warn_deprecated_module(__name__, "qcodes.dataset") diff --git a/src/qcodes/dataset/dond/do2d_retrace.py b/src/qcodes/dataset/dond/do2d_retrace.py index 7961f2a08091..edef5c178a42 100644 --- a/src/qcodes/dataset/dond/do2d_retrace.py +++ b/src/qcodes/dataset/dond/do2d_retrace.py @@ -1,149 +1,3 @@ -from __future__ import annotations +from qcodes.dataset._dond.do2d_retrace import do2d_retrace -import logging -from typing import TYPE_CHECKING - -from opentelemetry import trace - -from qcodes import config -from qcodes.dataset import ( - DataSetDefinition, - LinSweep, - LinSweeper, - datasaver_builder, - dond_into, -) - -LOG = logging.getLogger(__name__) -TRACER = trace.get_tracer(__name__) - - -if TYPE_CHECKING: - from collections.abc import Sequence - - from qcodes.dataset.data_set_protocol import DataSetProtocol - from qcodes.dataset.dond.do_nd_utils import ActionsT - from qcodes.dataset.experiment_container import Experiment - from qcodes.parameters import ParameterBase - - -@TRACER.start_as_current_span("qcodes.dataset.do2d") -def do2d_retrace( - param_set1: ParameterBase, - start1: float, - stop1: float, - num_points1: int, - delay1: float, - param_set2: ParameterBase, - start2: float, - stop2: float, - num_points2: int, - delay2: float, - *param_meas: ParameterBase, - enter_actions: ActionsT = (), - exit_actions: ActionsT = (), - before_inner_actions: ActionsT = (), - after_inner_actions: ActionsT = (), - measurement_name: str = "", - exp: Experiment | None = None, - additional_setpoints: Sequence[ParameterBase] = (), - show_progress: bool | None = None, -) -> tuple[DataSetProtocol, DataSetProtocol]: - """ - Perform a 1D scan of ``param_set1`` from ``start1`` to ``stop1`` in - ``num_points1`` and ``param_set2`` from ``start2`` to ``stop2`` in - ``num_points2`` measuring param_meas at each step. - - Args: - param_set1: The QCoDeS parameter to sweep over in the outer loop - start1: Starting point of sweep in outer loop - stop1: End point of sweep in the outer loop - num_points1: Number of points to measure in the outer loop - delay1: Delay after setting parameter in the outer loop - param_set2: The QCoDeS parameter to sweep over in the inner loop - start2: Starting point of sweep in inner loop - stop2: End point of sweep in the inner loop - num_points2: Number of points to measure in the inner loop - delay2: Delay after setting parameter before measurement is performed - param_meas: Parameter(s) to measure at each step or functions that - will be called at each step. The function should take no arguments. - The parameters and functions are called in the order they are - supplied. - enter_actions: A list of functions taking no arguments that will be - called before the measurements start - exit_actions: A list of functions taking no arguments that will be - called after the measurements ends - before_inner_actions: Actions executed before each run of the inner loop - after_inner_actions: Actions executed after each run of the inner loop - measurement_name: Name of the measurement. This will be passed down to - the dataset produced by the measurement. If not given, a default - value of 'results' is used for the dataset. - exp: The experiment to use for this measurement. - additional_setpoints: A list of setpoint parameters to be registered in - the measurement but not scanned. - show_progress: should a progress bar be displayed during the - measurement. If None the setting will be read from ``qcodesrc.json`` - - Returns: - The QCoDeS dataset. - - """ - - if show_progress is None: - show_progress = config.dataset.dond_show_progress - - dataset_definition = [ - DataSetDefinition( - name=measurement_name, - independent=[param_set1, param_set2], - dependent=param_meas, - experiment=exp, - ), - DataSetDefinition( - name=f"retrace {measurement_name}", - independent=[param_set1, param_set2], - dependent=param_meas, - experiment=exp, - ), - ] - - with datasaver_builder(dataset_definition) as datasavers: - for action in enter_actions: - action() - for _ in LinSweeper(param_set1, start1, stop1, num_points1, delay1): - sweep_up = LinSweep( - param_set2, - start2, - stop2, - num_points2, - delay2, - post_actions=after_inner_actions, - ) - sweep_down = LinSweep( - param_set2, - stop2, - start2, - num_points2, - delay2, - post_actions=after_inner_actions, - ) - for action in before_inner_actions: - action() - dond_into( - datasavers[0], - sweep_up, - *param_meas, - additional_setpoints=additional_setpoints, - ) - for action in before_inner_actions: - action() - dond_into( - datasavers[1], - sweep_down, - *param_meas, - additional_setpoints=additional_setpoints, - ) - for action in exit_actions: - action() - datasets = (datasavers[0].dataset, datasavers[1].dataset) - return datasets +__all__ = ["do2d_retrace"] diff --git a/src/qcodes/dataset/dond/do_0d.py b/src/qcodes/dataset/dond/do_0d.py index ff13871b4f3f..d03a5bf7a991 100644 --- a/src/qcodes/dataset/dond/do_0d.py +++ b/src/qcodes/dataset/dond/do_0d.py @@ -1,50 +1,3 @@ -from __future__ import annotations +from qcodes.dataset._dond.do_0d import do0d -import logging -from typing import TYPE_CHECKING, cast - -from opentelemetry import trace - -from .do_nd import DondKWargs, dond - -if TYPE_CHECKING: - from typing import Unpack - - from .do_nd_utils import ( - AxesTupleListWithDataSet, - ParamMeasT, - ) - -LOG = logging.getLogger(__name__) -TRACER = trace.get_tracer(__name__) - - -@TRACER.start_as_current_span("qcodes.dataset.do0d") -def do0d( - *param_meas: ParamMeasT, **kwargs: Unpack[DondKWargs] -) -> AxesTupleListWithDataSet: - """ - Perform a measurement of a single parameter. This is probably most - useful for a ParameterWithSetpoints that already returns an array of data points. - - Args: - *param_meas: Parameter(s) to measure at each step or functions that - will be called at each step. The function should take no arguments. - The parameters and functions are called in the order they are - supplied. - **kwargs: kwargs are the same as for :func:`dond` and forwarded directly to :func:`dond`. - - Returns: - The QCoDeS dataset. - - """ - - kwargs.setdefault("log_info", "Using 'qcodes.dataset.do0d'") - - # since we only support entering parameters - # as a simple list or args we are sure to always - # get back a AxesTupleListWithDataSet and cast is safe - return cast( - "AxesTupleListWithDataSet", - dond(*param_meas, **kwargs, squeeze=True), - ) +__all__ = ["do0d"] diff --git a/src/qcodes/dataset/dond/do_1d.py b/src/qcodes/dataset/dond/do_1d.py index d453148c58ba..73bcd71f9d5f 100644 --- a/src/qcodes/dataset/dond/do_1d.py +++ b/src/qcodes/dataset/dond/do_1d.py @@ -1,71 +1,3 @@ -from __future__ import annotations +from qcodes.dataset._dond.do_1d import do1d -import logging -from typing import TYPE_CHECKING, cast - -from opentelemetry import trace - -from .do_nd import DondKWargs, dond -from .sweeps import LinSweep - -if TYPE_CHECKING: - from typing import Unpack - - from qcodes.dataset.dond.do_nd_utils import ( - AxesTupleListWithDataSet, - ParamMeasT, - ) - from qcodes.parameters import ParameterBase - -LOG = logging.getLogger(__name__) -TRACER = trace.get_tracer(__name__) - - -@TRACER.start_as_current_span("qcodes.dataset.do1d") -def do1d( - param_set: ParameterBase, - start: float, - stop: float, - num_points: int, - delay: float, - *param_meas: ParamMeasT, - **kwargs: Unpack[DondKWargs], -) -> AxesTupleListWithDataSet: - """ - Perform a 1D scan of ``param_set`` from ``start`` to ``stop`` in - ``num_points`` measuring param_meas at each step. In case param_meas is - an ArrayParameter this is effectively a 2d scan. - - Args: - param_set: The QCoDeS parameter to sweep over - start: Starting point of sweep - stop: End point of sweep - num_points: Number of points in sweep - delay: Delay after setting parameter before measurement is performed - *param_meas: Parameter(s) to measure at each step or functions that - will be called at each step. The function should take no arguments. - The parameters and functions are called in the order they are - supplied. - **kwargs: kwargs are the same as for :func:`dond` and forwarded directly to :func:`dond`. - - Returns: - The QCoDeS dataset. - - """ - kwargs.setdefault("log_info", "Using 'qcodes.dataset.do1d'") - - return cast( - "AxesTupleListWithDataSet", - dond( - LinSweep( - param=param_set, - start=start, - stop=stop, - delay=delay, - num_points=num_points, - ), - *param_meas, - **kwargs, - squeeze=True, - ), - ) +__all__ = ["do1d"] diff --git a/src/qcodes/dataset/dond/do_2d.py b/src/qcodes/dataset/dond/do_2d.py index 55fe5fb566fc..60b60c444305 100644 --- a/src/qcodes/dataset/dond/do_2d.py +++ b/src/qcodes/dataset/dond/do_2d.py @@ -1,225 +1,3 @@ -from __future__ import annotations +from qcodes.dataset._dond.do_2d import do2d -import logging -import sys -import time -from typing import TYPE_CHECKING, cast - -import numpy as np -from opentelemetry import trace -from tqdm.auto import tqdm - -from qcodes import config -from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement -from qcodes.dataset.dond.do_nd_utils import ( - BreakConditionInterrupt, - _handle_plotting, - _register_actions, - _register_parameters, - _set_write_period, - catch_interrupts, -) -from qcodes.dataset.measurements import Measurement -from qcodes.dataset.threading import ( - SequentialParamsCaller, - ThreadPoolParamsCaller, - process_params_meas, -) -from qcodes.parameters import ParameterBase - -LOG = logging.getLogger(__name__) -TRACER = trace.get_tracer(__name__) - - -if TYPE_CHECKING: - from collections.abc import Sequence - - from qcodes.dataset.descriptions.versioning.rundescribertypes import Shapes - from qcodes.dataset.dond.do_nd_utils import ( - ActionsT, - AxesTupleListWithDataSet, - BreakConditionT, - ParamMeasT, - ) - from qcodes.dataset.experiment_container import Experiment - - -@TRACER.start_as_current_span("qcodes.dataset.do2d") -def do2d( - param_set1: ParameterBase, - start1: float, - stop1: float, - num_points1: int, - delay1: float, - param_set2: ParameterBase, - start2: float, - stop2: float, - num_points2: int, - delay2: float, - *param_meas: ParamMeasT, - set_before_sweep: bool | None = True, - enter_actions: ActionsT = (), - exit_actions: ActionsT = (), - before_inner_actions: ActionsT = (), - after_inner_actions: ActionsT = (), - write_period: float | None = None, - measurement_name: str = "", - exp: Experiment | None = None, - flush_columns: bool = False, - do_plot: bool | None = None, - use_threads: bool | None = None, - additional_setpoints: Sequence[ParameterBase] = (), - show_progress: bool | None = None, - log_info: str | None = None, - break_condition: BreakConditionT | None = None, -) -> AxesTupleListWithDataSet: - """ - Perform a 1D scan of ``param_set1`` from ``start1`` to ``stop1`` in - ``num_points1`` and ``param_set2`` from ``start2`` to ``stop2`` in - ``num_points2`` measuring param_meas at each step. - - Args: - param_set1: The QCoDeS parameter to sweep over in the outer loop - start1: Starting point of sweep in outer loop - stop1: End point of sweep in the outer loop - num_points1: Number of points to measure in the outer loop - delay1: Delay after setting parameter in the outer loop - param_set2: The QCoDeS parameter to sweep over in the inner loop - start2: Starting point of sweep in inner loop - stop2: End point of sweep in the inner loop - num_points2: Number of points to measure in the inner loop - delay2: Delay after setting parameter before measurement is performed - param_meas: Parameter(s) to measure at each step or functions that - will be called at each step. The function should take no arguments. - The parameters and functions are called in the order they are - supplied. - set_before_sweep: if True the outer parameter is set to its first value - before the inner parameter is swept to its next value. - enter_actions: A list of functions taking no arguments that will be - called before the measurements start - exit_actions: A list of functions taking no arguments that will be - called after the measurements ends - before_inner_actions: Actions executed before each run of the inner loop - after_inner_actions: Actions executed after each run of the inner loop - write_period: The time after which the data is actually written to the - database. - measurement_name: Name of the measurement. This will be passed down to - the dataset produced by the measurement. If not given, a default - value of 'results' is used for the dataset. - exp: The experiment to use for this measurement. - flush_columns: The data is written after a column is finished - independent of the passed time and write period. - additional_setpoints: A list of setpoint parameters to be registered in - the measurement but not scanned. - do_plot: should png and pdf versions of the images be saved after the - run. If None the setting will be read from ``qcodesrc.json`` - use_threads: If True measurements from each instrument will be done on - separate threads. If you are measuring from several instruments - this may give a significant speedup. - show_progress: should a progress bar be displayed during the - measurement. If None the setting will be read from ``qcodesrc.json`` - log_info: Message that is logged during the measurement. If None a default - message is used. - break_condition: Callable that takes no arguments. If returned True, - measurement is interrupted. - - Returns: - The QCoDeS dataset. - - """ - - if do_plot is None: - do_plot = cast("bool", config.dataset.dond_plot) - if show_progress is None: - show_progress = config.dataset.dond_show_progress - - meas = Measurement(name=measurement_name, exp=exp) - if log_info is not None: - meas._extra_log_info = log_info - else: - meas._extra_log_info = "Using 'qcodes.dataset.do2d'" - all_setpoint_params = ( - param_set1, - param_set2, - *tuple(s for s in additional_setpoints), - ) - - measured_parameters = tuple( - param for param in param_meas if isinstance(param, ParameterBase) - ) - - try: - loop_shape = (num_points1, num_points2, *tuple(1 for _ in additional_setpoints)) - shapes: Shapes | None = detect_shape_of_measurement( - measured_parameters, loop_shape - ) - except TypeError: - LOG.exception( - f"Could not detect shape of {measured_parameters} " - f"falling back to unknown shape." - ) - shapes = None - - _register_parameters(meas, all_setpoint_params) - _register_parameters(meas, param_meas, setpoints=all_setpoint_params, shapes=shapes) - _set_write_period(meas, write_period) - _register_actions(meas, enter_actions, exit_actions) - - if use_threads is None: - use_threads = config.dataset.use_threads - - param_meas_caller = ( - ThreadPoolParamsCaller(*param_meas) - if use_threads - else SequentialParamsCaller(*param_meas) - ) - - with ( - catch_interrupts() as interrupted, - meas.run() as datasaver, - param_meas_caller as call_param_meas, - ): - dataset = datasaver.dataset - additional_setpoints_data = process_params_meas(additional_setpoints) - setpoints1 = np.linspace(start1, stop1, num_points1) - for set_point1 in tqdm(setpoints1, disable=not show_progress): - if set_before_sweep: - param_set2.set(start2) - - param_set1.set(set_point1) - - for action in before_inner_actions: - action() - - time.sleep(delay1) - - setpoints2 = np.linspace(start2, stop2, num_points2) - - # flush to prevent unflushed print's to visually interrupt tqdm bar - # updates - sys.stdout.flush() - sys.stderr.flush() - for set_point2 in tqdm(setpoints2, disable=not show_progress, leave=False): - # skip first inner set point if `set_before_sweep` - if set_point2 == start2 and set_before_sweep: - pass - else: - param_set2.set(set_point2) - time.sleep(delay2) - - datasaver.add_result( - (param_set1, set_point1), - (param_set2, set_point2), - *call_param_meas(), - *additional_setpoints_data, - ) - - if callable(break_condition) and break_condition(): - raise BreakConditionInterrupt("Break condition was met.") - - for action in after_inner_actions: - action() - if flush_columns: - datasaver.flush_data_to_database() - - return _handle_plotting(dataset, do_plot, interrupted()) +__all__ = ["do2d"] diff --git a/src/qcodes/dataset/dond/do_nd.py b/src/qcodes/dataset/dond/do_nd.py index 43ca9b77ae2d..7918fb25f8af 100644 --- a/src/qcodes/dataset/dond/do_nd.py +++ b/src/qcodes/dataset/dond/do_nd.py @@ -1,889 +1,8 @@ -from __future__ import annotations - -import itertools -import logging -import time -from collections.abc import Callable, Mapping, Sequence -from contextlib import ExitStack -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Literal, NotRequired, cast, overload - -import numpy as np -from opentelemetry import trace -from tqdm.auto import tqdm -from typing_extensions import TypedDict - -from qcodes import config -from qcodes.dataset.descriptions.detect_shapes import detect_shape_of_measurement -from qcodes.dataset.dond.do_nd_utils import ( - BreakConditionInterrupt, - _handle_plotting, - _register_actions, - _register_parameters, - _set_write_period, - catch_interrupts, -) -from qcodes.dataset.measurements import Measurement -from qcodes.dataset.threading import ( - SequentialParamsCaller, - ThreadPoolParamsCaller, - process_params_meas, +from qcodes.dataset._dond.do_nd import ( + DondKWargs, + ParameterGroup, + ParameterSetEvent, + dond, ) -from qcodes.parameters import ParameterBase - -from .sweeps import AbstractSweep, TogetherSweep - -if TYPE_CHECKING: - from qcodes.dataset.descriptions.versioning.rundescribertypes import Shapes - from qcodes.dataset.dond.do_nd_utils import ( - ActionsT, - AxesTupleListWithDataSet, - BreakConditionT, - MultiAxesTupleListWithDataSet, - ParamMeasT, - ) - from qcodes.dataset.experiment_container import Experiment - -LOG = logging.getLogger(__name__) -SweepVarType = Any - -TRACER = trace.get_tracer(__name__) - - -class ParameterGroup(TypedDict): - params: tuple[ParamMeasT, ...] - meas_name: str - - -@dataclass -class ParameterSetEvent: - parameter: ParameterBase - new_value: SweepVarType - should_set: bool - delay: float - actions: ActionsT - get_after_set: bool - - -class _Sweeper: - def __init__( - self, - sweeps: Sequence[AbstractSweep | TogetherSweep], - additional_setpoints: Sequence[ParameterBase], - ): - self._additional_setpoints = additional_setpoints - self._sweeps = sweeps - self._setpoints = self._make_setpoints_tuples() - self._setpoints_dict = self._make_setpoints_dict() - self._shape = self._make_shape(sweeps, additional_setpoints) - self._iter_index = 0 - - @property - def setpoints_dict(self) -> dict[str, list[Any]]: - return self._setpoints_dict - - def _make_setpoints_tuples( - self, - ) -> tuple[tuple[tuple[SweepVarType, ...] | SweepVarType, ...], ...]: - sweeps = tuple(sweep.get_setpoints() for sweep in self._sweeps) - return cast( - "tuple[tuple[tuple[SweepVarType, ...] | SweepVarType, ...], ...]", - tuple(itertools.product(*sweeps)), - ) - - def _make_single_point_setpoints_dict(self, index: int) -> dict[str, SweepVarType]: - setpoint_dict = {} - values = self._setpoints[index] - for sweep, subvalues in zip(self._sweeps, values): - if isinstance(sweep, TogetherSweep): - for individual_sweep, value in zip(sweep.sweeps, subvalues): - setpoint_dict[individual_sweep.param.full_name] = value - else: - setpoint_dict[sweep.param.full_name] = subvalues - return setpoint_dict - - def _make_setpoints_dict(self) -> dict[str, list[Any]]: - setpoint_dict: dict[str, list[SweepVarType]] = {} - - for sweep in self._sweeps: - if isinstance(sweep, TogetherSweep): - for individual_sweep in sweep.sweeps: - setpoint_dict[individual_sweep.param.full_name] = [] - else: - setpoint_dict[sweep.param.full_name] = [] - - for setpoint_tuples in self._setpoints: - for sweep, values in zip(self._sweeps, setpoint_tuples): - if isinstance(sweep, TogetherSweep): - for individual_sweep, individual_value in zip(sweep.sweeps, values): - setpoint_dict[individual_sweep.param.full_name].append( - individual_value - ) - else: - setpoint_dict[sweep.param.full_name].append(values) - return setpoint_dict - - @property - def all_sweeps(self) -> tuple[AbstractSweep, ...]: - sweeps: list[AbstractSweep] = [] - for sweep in self._sweeps: - if isinstance(sweep, TogetherSweep): - sweeps.extend(sweep.sweeps) - else: - sweeps.append(sweep) - return tuple(sweeps) - - @property - def all_setpoint_params(self) -> tuple[ParameterBase, ...]: - return tuple(sweep.param for sweep in self.all_sweeps) + tuple( - self._additional_setpoints - ) - - @property - def sweep_groupes(self) -> tuple[tuple[ParameterBase, ...], ...]: - """ - These are all the combinations of setpoints we consider - valid for setpoints in a dataset. A dataset must depend on - at least one parameter from each dimension of the dond. - For dimensions that uses TogetherSweep they may depend - on one or more of the parameters of that sweep. - """ - param_tuple_list: list[tuple[ParameterBase, ...]] = [] - for sweep in self._sweeps: - if isinstance(sweep, TogetherSweep): - param_tuple_list.append( - tuple(sub_sweep.param for sub_sweep in sweep.sweeps) - ) - else: - param_tuple_list.append((sweep.param,)) - - param_tuple_list.extend( - [(setpoint,) for setpoint in self._additional_setpoints] - ) - - # in param_tuple_list there is a tuple of possible setpoints for each - # dim in the dond. For regular sweeps this is a 1 tuple but for - # a TogetherSweep it is of length of the number of parameters. - - # now we expand to a list of setpoints in a TogetherSweep - # to a list of all possible combinations of these. - - expanded_param_tuples = tuple( - tuple( - itertools.chain.from_iterable( - itertools.combinations(param_tuple, j + 1) - for j in range(len(param_tuple)) - ) - ) - for param_tuple in param_tuple_list - ) - - # next we generate all valid combinations of picking one parameter from each - # dimension in the setpoints. - setpoint_combinations = itertools.product(*expanded_param_tuples) - - setpoint_combinations_expanded = tuple( - tuple(itertools.chain.from_iterable(setpoint_combination)) - for setpoint_combination in setpoint_combinations - ) - - return setpoint_combinations_expanded - - @staticmethod - def _make_shape( - sweeps: Sequence[AbstractSweep | TogetherSweep], - addtional_setpoints: Sequence[ParameterBase], - ) -> tuple[int, ...]: - loop_shape = tuple(sweep.num_points for sweep in sweeps) + tuple( - 1 for _ in addtional_setpoints - ) - return loop_shape - - @property - def shape(self) -> tuple[int, ...]: - return self._shape - - def __getitem__(self, index: int) -> tuple[ParameterSetEvent, ...]: - setpoints = self._make_single_point_setpoints_dict(index) - - if index == 0: - previous_setpoints: dict[str, SweepVarType | None] = {} - for key in setpoints: - previous_setpoints[key] = None - else: - previous_setpoints = self._make_single_point_setpoints_dict(index - 1) - - parameter_set_events = [] - - for sweep in self.all_sweeps: - new_value = setpoints[sweep.param.full_name] - old_value = previous_setpoints[sweep.param.full_name] - if old_value is None or old_value != new_value: - should_set = True - else: - should_set = False - event = ParameterSetEvent( - new_value=new_value, - parameter=sweep.param, - should_set=should_set, - delay=sweep.delay, - actions=sweep.post_actions, - get_after_set=sweep.get_after_set, - ) - parameter_set_events.append(event) - return tuple(parameter_set_events) - - def __len__(self) -> int: - return int(np.prod(self.shape)) - - def __iter__(self) -> _Sweeper: - return self - - def __next__(self) -> tuple[ParameterSetEvent, ...]: - if self._iter_index < len(self): - return_val = self[self._iter_index] - self._iter_index += 1 - return return_val - else: - raise StopIteration - - -class _Measurements: - def __init__( - self, - sweeper: _Sweeper, - measurement_name: str | Sequence[str], - params_meas: Sequence[ParamMeasT | Sequence[ParamMeasT]], - enter_actions: ActionsT, - exit_actions: ActionsT, - experiments: Experiment | Sequence[Experiment] | None, - write_period: float | None, - log_info: str | None, - dataset_dependencies: Mapping[str, Sequence[ParamMeasT]] | None = None, - ): - self._sweeper = sweeper - self._enter_actions = enter_actions - self._exit_actions = exit_actions - self._write_period = write_period - self._log_info = log_info - if log_info is not None: - self._extra_log_info = log_info - else: - self._extra_log_info = "Using 'qcodes.dataset.dond'" - - ( - self._measured_all, - grouped_parameters, - self._measured_parameters, - ) = self._extract_parameters_by_type_and_group(params_meas) - - self._shapes = self._get_shapes() - - if dataset_dependencies and len(grouped_parameters) > 1: - raise ValueError( - "Measured parameters have been grouped both in input " - "and given in dataset dependencies. This is not supported, " - "group measurement parameters either in input or in " - "dataset dependencies." - ) - - if dataset_dependencies is None: - self._groups = self._create_groups_from_grouped_parameters( - grouped_parameters, experiments, measurement_name - ) - elif dataset_dependencies: - _validate_dataset_dependencies_and_names( - dataset_dependencies, measurement_name - ) - dataset_dependencies_split = self._split_dateset_dependencies( - dataset_dependencies - ) - self._groups = self._create_groups_from_dataset_dependencies( - dataset_dependencies_split, - self._measured_parameters, - experiments, - measurement_name, - ) - - @property - def measured_all(self) -> tuple[ParamMeasT, ...]: - return self._measured_all - - @property - def measured_parameters(self) -> tuple[ParameterBase, ...]: - return self._measured_parameters - - @property - def shapes(self) -> Shapes | None: - return self._shapes - - @property - def groups(self) -> tuple[_SweepMeasGroup, ...]: - return self._groups - - @staticmethod - def _create_measurement_names( - measurement_name: str | Sequence[str], n_names_required: int - ) -> tuple[str, ...]: - if isinstance(measurement_name, str): - return (measurement_name,) * n_names_required - else: - if len(measurement_name) != n_names_required: - raise ValueError( - f"Got {len(measurement_name)} measurement names " - f"but should create {n_names_required} dataset(s)." - ) - return tuple(measurement_name) - - @staticmethod - def _extract_parameters_by_type_and_group( - params_meas: Sequence[ParamMeasT | Sequence[ParamMeasT]], - ) -> tuple[ - tuple[ParamMeasT, ...], - tuple[tuple[ParamMeasT, ...], ...], - tuple[ParameterBase, ...], - ]: - measured_parameters: list[ParameterBase] = [] - measured_all: list[ParamMeasT] = [] - single_group: list[ParamMeasT] = [] - multi_group: list[tuple[ParamMeasT, ...]] = [] - grouped_parameters: tuple[tuple[ParamMeasT, ...], ...] - for param in params_meas: - if not isinstance(param, Sequence): - single_group.append(param) - measured_all.append(param) - if isinstance(param, ParameterBase): - measured_parameters.append(param) - elif not isinstance(param, str): - multi_group.append(tuple(param)) - for nested_param in param: - measured_all.append(nested_param) - if isinstance(nested_param, ParameterBase): - measured_parameters.append(nested_param) - - if single_group and multi_group: - raise ValueError( - f"Got both grouped and non grouped " - f"parameters to measure in " - f"{params_meas}. This is not supported." - ) - - if single_group: - grouped_parameters = (tuple(single_group),) - elif multi_group: - grouped_parameters = tuple(multi_group) - else: - raise ValueError("No parameters to measure supplied") - - return tuple(measured_all), grouped_parameters, tuple(measured_parameters) - - def _get_shapes(self) -> Shapes | None: - try: - shapes: Shapes | None = detect_shape_of_measurement( - self.measured_parameters, self._sweeper.shape - ) - LOG.debug("Detected shapes to be %s", shapes) - except TypeError: - LOG.exception( - f"Could not detect shape of {self.measured_parameters} " - f"falling back to unknown shape." - ) - shapes = None - return shapes - - @staticmethod - def _get_experiments( - experiments: Experiment | Sequence[Experiment] | None, - n_experiments_required: int, - ) -> Sequence[Experiment | None]: - if not isinstance(experiments, Sequence): - experiments_internal: Sequence[Experiment | None] = [ - experiments - ] * n_experiments_required - else: - experiments_internal = experiments - - if len(experiments_internal) != n_experiments_required: - raise ValueError( - f"Inconsistent number of " - f"datasets and experiments, " - f"got {n_experiments_required} and {len(experiments_internal)}." - ) - return experiments_internal - - def _create_groups_from_grouped_parameters( - self, - grouped_parameters: tuple[tuple[ParamMeasT, ...], ...], - experiments: Experiment | Sequence[Experiment] | None, - meas_names: str | Sequence[str], - ) -> tuple[_SweepMeasGroup, ...]: - setpoints = self._sweeper.all_setpoint_params - - groups = [] - m_group: Sequence[ParamMeasT] - - experiments_internal = self._get_experiments( - experiments, len(grouped_parameters) - ) - measurement_names = self._create_measurement_names( - meas_names, len(grouped_parameters) - ) - - for m_group, experiment, meas_name in zip( - grouped_parameters, - experiments_internal, - measurement_names, - ): - meas_ctx = self._create_measurement_ctx_manager( - experiment, meas_name, setpoints, tuple(m_group) - ) - s_m_group = _SweepMeasGroup(setpoints, tuple(m_group), meas_ctx) - groups.append(s_m_group) - return tuple(groups) - - def _create_groups_from_dataset_dependencies( - self, - dataset_dependencies: dict[ - str, tuple[Sequence[ParameterBase], Sequence[ParamMeasT]] - ], - all_measured_parameters: tuple[ParameterBase, ...], - experiments: Experiment | Sequence[Experiment] | None, - meas_names: str | Sequence[str], - ) -> tuple[_SweepMeasGroup, ...]: - potential_setpoint_groups = self._sweeper.sweep_groupes - - experiments_internal = self._get_experiments( - experiments, len(dataset_dependencies) - ) - if meas_names == "": - meas_names = tuple(dataset_dependencies.keys()) - - measurement_names = self._create_measurement_names( - meas_names, len(dataset_dependencies) - ) - - all_dataset_dependencies_meas_parameters = tuple( - itertools.chain.from_iterable( - output[1] for output in dataset_dependencies.values() - ) - ) - - for meas_param in all_measured_parameters: - if meas_param not in all_dataset_dependencies_meas_parameters: - raise ValueError( - f"Parameter {meas_param} is measured but not added " - f"to any dataset in dataset_dependencies." - ) - - groups = [] - for experiment, meas_name in zip( - experiments_internal, - measurement_names, - ): - (sp_group, m_group) = dataset_dependencies[meas_name] - if tuple(sp_group) not in potential_setpoint_groups: - raise ValueError( - f"dataset_dependencies contains {sp_group} " - f"which is not among the expected groups of setpoints " - f"{potential_setpoint_groups}" - ) - - LOG.info( - f"creating context manager for setpoints" - f" {sp_group} and measurement parameters {m_group}" - ) - meas_ctx = self._create_measurement_ctx_manager( - experiment, meas_name, tuple(sp_group), tuple(m_group) - ) - s_m_group = _SweepMeasGroup(tuple(sp_group), tuple(m_group), meas_ctx) - groups.append(s_m_group) - return tuple(groups) - - def _create_measurement_ctx_manager( - self, - experiment: Experiment | None, - measurement_name: str, - sweep_parameters: Sequence[ParameterBase], - measure_parameters: Sequence[ParamMeasT], - ) -> Measurement: - meas = Measurement(name=measurement_name, exp=experiment) - _register_parameters(meas, sweep_parameters) - _register_parameters( - meas, - measure_parameters, - setpoints=sweep_parameters, - shapes=self.shapes, - ) - meas._extra_log_info = self._extra_log_info - _set_write_period(meas, self._write_period) - _register_actions(meas, self._enter_actions, self._exit_actions) - return meas - - def _split_dateset_dependencies( - self, - dataset_dependencies: Mapping[str, Sequence[ParamMeasT]], - ) -> dict[str, tuple[Sequence[ParameterBase], Sequence[ParamMeasT]]]: - # split measured parameters from setpoint parameters using param_meas - dataset_dependencies_split: dict[ - str, tuple[Sequence[ParameterBase], Sequence[ParamMeasT]] - ] = {} - for name, dataset_parameters in dataset_dependencies.items(): - meas_parameters = tuple( - param for param in dataset_parameters if param in self.measured_all - ) - setpoint_parameters = cast( - "Sequence[ParameterBase]", - tuple( - param - for param in dataset_parameters - if param not in self.measured_all - ), - ) - dataset_dependencies_split[name] = ( - setpoint_parameters, - meas_parameters, - ) - return dataset_dependencies_split - - -# idealy we would want this to be frozen but then postinit -# cannot calculate all the parameters -# https://stackoverflow.com/questions/53756788/ -@dataclass(frozen=False) -class _SweepMeasGroup: - sweep_parameters: tuple[ParameterBase, ...] - measure_parameters: tuple[ParamMeasT, ...] - measurement_cxt: Measurement - - def __post_init__(self) -> None: - meas_parameters = tuple( - a for a in self.measure_parameters if isinstance(a, ParameterBase) - ) - self._parameters = self.sweep_parameters + meas_parameters - - @property - def parameters(self) -> tuple[ParameterBase, ...]: - return self._parameters - - -class DondKWargs(TypedDict): - write_period: NotRequired[float | None] - measurement_name: NotRequired[str | Sequence[str]] - exp: NotRequired[Experiment | Sequence[Experiment] | None] - enter_actions: NotRequired[ActionsT] - exit_actions: NotRequired[ActionsT] - do_plot: NotRequired[bool | None] - show_progress: NotRequired[bool | None] - use_threads: NotRequired[bool | None] - additional_setpoints: NotRequired[Sequence[ParameterBase]] - log_info: NotRequired[str | None] - break_condition: NotRequired[BreakConditionT | None] - dataset_dependencies: NotRequired[Mapping[str, Sequence[ParamMeasT]]] - in_memory_cache: NotRequired[bool | None] - - -@overload -def dond( - *params: AbstractSweep | TogetherSweep | ParamMeasT | Sequence[ParamMeasT], - write_period: float | None = None, - measurement_name: str | Sequence[str] = "", - exp: Experiment | Sequence[Experiment] | None = None, - enter_actions: ActionsT = (), - exit_actions: ActionsT = (), - do_plot: bool | None = None, - show_progress: bool | None = None, - use_threads: bool | None = None, - additional_setpoints: Sequence[ParameterBase] = (), - log_info: str | None = None, - break_condition: BreakConditionT | None = None, - dataset_dependencies: Mapping[str, Sequence[ParamMeasT]] | None = None, - in_memory_cache: bool | None = None, - squeeze: Literal[False], -) -> MultiAxesTupleListWithDataSet: ... - - -@overload -def dond( - *params: AbstractSweep | TogetherSweep | ParamMeasT | Sequence[ParamMeasT], - write_period: float | None = None, - measurement_name: str | Sequence[str] = "", - exp: Experiment | Sequence[Experiment] | None = None, - enter_actions: ActionsT = (), - exit_actions: ActionsT = (), - do_plot: bool | None = None, - show_progress: bool | None = None, - use_threads: bool | None = None, - additional_setpoints: Sequence[ParameterBase] = (), - log_info: str | None = None, - break_condition: BreakConditionT | None = None, - dataset_dependencies: Mapping[str, Sequence[ParamMeasT]] | None = None, - in_memory_cache: bool | None = None, - squeeze: Literal[True], -) -> AxesTupleListWithDataSet | MultiAxesTupleListWithDataSet: ... - - -@overload -def dond( - *params: AbstractSweep | TogetherSweep | ParamMeasT | Sequence[ParamMeasT], - write_period: float | None = None, - measurement_name: str | Sequence[str] = "", - exp: Experiment | Sequence[Experiment] | None = None, - enter_actions: ActionsT = (), - exit_actions: ActionsT = (), - do_plot: bool | None = None, - show_progress: bool | None = None, - use_threads: bool | None = None, - additional_setpoints: Sequence[ParameterBase] = (), - log_info: str | None = None, - break_condition: BreakConditionT | None = None, - dataset_dependencies: Mapping[str, Sequence[ParamMeasT]] | None = None, - in_memory_cache: bool | None = None, - squeeze: bool = True, -) -> AxesTupleListWithDataSet | MultiAxesTupleListWithDataSet: ... - - -@TRACER.start_as_current_span("qcodes.dataset.dond") -def dond( - *params: AbstractSweep | TogetherSweep | ParamMeasT | Sequence[ParamMeasT], - write_period: float | None = None, - measurement_name: str | Sequence[str] = "", - exp: Experiment | Sequence[Experiment] | None = None, - enter_actions: ActionsT = (), - exit_actions: ActionsT = (), - do_plot: bool | None = None, - show_progress: bool | None = None, - use_threads: bool | None = None, - additional_setpoints: Sequence[ParameterBase] = (), - log_info: str | None = None, - break_condition: BreakConditionT | None = None, - dataset_dependencies: Mapping[str, Sequence[ParamMeasT]] | None = None, - in_memory_cache: bool | None = None, - squeeze: bool = True, -) -> AxesTupleListWithDataSet | MultiAxesTupleListWithDataSet: - """ - Perform n-dimentional scan from slowest (first) to the fastest (last), to - measure m measurement parameters. The dimensions should be specified - as sweep objects, and after them the parameters to measure should be passed. - - Args: - params: Instances of n sweep classes and m measurement parameters, - e.g. if linear sweep is considered: - - .. code-block:: - - LinSweep(param_set_1, start_1, stop_1, num_points_1, delay_1), ..., - LinSweep(param_set_n, start_n, stop_n, num_points_n, delay_n), - param_meas_1, param_meas_2, ..., param_meas_m - - If multiple DataSets creation is needed, measurement parameters should - be grouped, so one dataset will be created for each group. e.g.: - - .. code-block:: - - LinSweep(param_set_1, start_1, stop_1, num_points_1, delay_1), ..., - LinSweep(param_set_n, start_n, stop_n, num_points_n, delay_n), - [param_meas_1, param_meas_2], ..., [param_meas_m] - - If you want to sweep multiple parameters together. - - .. code-block:: - - TogetherSweep(LinSweep(param_set_1, start_1, stop_1, num_points, delay_1), - LinSweep(param_set_2, start_2, stop_2, num_points, delay_2)) - param_meas_1, param_meas_2, ..., param_meas_m - - - write_period: The time after which the data is actually written to the - database. - measurement_name: Name(s) of the measurement. This will be passed down to - the dataset produced by the measurement. If not given, a default - value of 'results' is used for the dataset. If more than one is - given, each dataset will have an individual name. - exp: The experiment to use for this measurement. If you create multiple - measurements using groups you may also supply multiple experiments. - enter_actions: A list of functions taking no arguments that will be - called before the measurements start. - exit_actions: A list of functions taking no arguments that will be - called after the measurements ends. - do_plot: should png and pdf versions of the images be saved and plots - are shown after the run. If None the setting will be read from - ``qcodesrc.json`` - show_progress: should a progress bar be displayed during the - measurement. If None the setting will be read from ``qcodesrc.json`` - use_threads: If True, measurements from each instrument will be done on - separate threads. If you are measuring from several instruments - this may give a significant speedup. - additional_setpoints: A list of setpoint parameters to be registered in - the measurement but not scanned/swept-over. - log_info: Message that is logged during the measurement. If None a default - message is used. - break_condition: Callable that takes no arguments. If returned True, - measurement is interrupted. - dataset_dependencies: Optionally describe that measured datasets only depend - on a subset of the setpoint parameters. Given as a mapping from - measurement names to Sequence of Parameters. Note that a dataset must - depend on at least one parameter from each dimension but can depend - on one or more parameters from a dimension sweeped with a TogetherSweep. - in_memory_cache: - Should a cache of the data be kept available in memory for faster - plotting and exporting. Useful to disable if the data is very large - in order to save on memory consumption. - If ``None``, the value for this will be read from ``qcodesrc.json`` config file. - squeeze: If True, will return a tuple of QCoDeS DataSet, Matplotlib axis, - Matplotlib colorbar if only one group of measurements was performed - and a tuple of tuples of these if more than one group of measurements - was performed. If False, will always return a tuple where the first - member is a tuple of QCoDeS DataSet(s) and the second member is a tuple - of Matplotlib axis(es) and the third member is a tuple of Matplotlib - colorbar(s). - - Returns: - A tuple of QCoDeS DataSet, Matplotlib axis, Matplotlib colorbar. If - more than one group of measurement parameters is supplied, the output - will be a tuple of tuple(QCoDeS DataSet), tuple(Matplotlib axis), - tuple(Matplotlib colorbar), in which each element of each sub-tuple - belongs to one group, and the order of elements is the order of - the supplied groups. - - """ - if do_plot is None: - do_plot = cast("bool", config.dataset.dond_plot) - if show_progress is None: - show_progress = config.dataset.dond_show_progress - - sweep_instances, params_meas = _parse_dond_arguments(*params) - - sweeper = _Sweeper(sweep_instances, additional_setpoints) - - measurements = _Measurements( - sweeper, - measurement_name, - params_meas, - enter_actions, - exit_actions, - exp, - write_period, - log_info, - dataset_dependencies, - ) - - LOG.info( - "Starting a doNd with scan with\n setpoints: %s,\n measuring: %s", - sweeper.all_setpoint_params, - measurements.measured_all, - ) - LOG.debug( - "dond has been grouped into the following datasets:\n%s", - measurements.groups, - ) - - datasets = [] - plots_axes = [] - plots_colorbar = [] - if use_threads is None: - use_threads = config.dataset.use_threads - - params_meas_caller = ( - ThreadPoolParamsCaller(*measurements.measured_all) - if use_threads - else SequentialParamsCaller(*measurements.measured_all) - ) - - datasavers = [] - interrupted: Callable[ # noqa: E731 - [], KeyboardInterrupt | BreakConditionInterrupt | None - ] = lambda: None - try: - with ( - catch_interrupts() as interrupted, - ExitStack() as stack, - params_meas_caller as call_params_meas, - ): - datasavers = [ - stack.enter_context( - group.measurement_cxt.run(in_memory_cache=in_memory_cache) - ) - for group in measurements.groups - ] - additional_setpoints_data = process_params_meas(additional_setpoints) - for set_events in tqdm(sweeper, disable=not show_progress): - LOG.debug("Processing set events: %s", set_events) - results: dict[ParameterBase, Any] = {} - for set_event in set_events: - if set_event.should_set: - set_event.parameter(set_event.new_value) - for act in set_event.actions: - act() - time.sleep(set_event.delay) - if set_event.get_after_set: - set_event.new_value = set_event.parameter() - - results[set_event.parameter] = set_event.new_value - - meas_value_pair = call_params_meas() - for meas_param, value in meas_value_pair: - results[meas_param] = value - - for datasaver, group in zip(datasavers, measurements.groups): - filtered_results_list = [ - (param, value) - for param, value in results.items() - if param in group.parameters - ] - datasaver.add_result( - *filtered_results_list, - *additional_setpoints_data, - ) - - if callable(break_condition) and break_condition(): - raise BreakConditionInterrupt("Break condition was met.") - finally: - for datasaver in datasavers: - ds, plot_axis, plot_color = _handle_plotting( - datasaver.dataset, do_plot, interrupted() - ) - datasets.append(ds) - plots_axes.append(plot_axis) - plots_colorbar.append(plot_color) - - if len(measurements.groups) == 1 and squeeze is True: - return datasets[0], plots_axes[0], plots_colorbar[0] - else: - return tuple(datasets), tuple(plots_axes), tuple(plots_colorbar) - - -def _validate_dataset_dependencies_and_names( - dataset_dependencies: Mapping[str, Sequence[ParamMeasT]] | None, - measurement_name: str | Sequence[str], -) -> None: - if dataset_dependencies is not None and measurement_name != "": - if isinstance(measurement_name, str): - raise ValueError( - "Creating multiple datasets but only one measurement name given." - ) - if set(dataset_dependencies.keys()) != set(measurement_name): - raise ValueError( - f"Inconsistent measurement names: measurement_name " - f"contains {measurement_name} " - f"but dataset_dependencies contains " - f"{tuple(dataset_dependencies.keys())}." - ) - -def _parse_dond_arguments( - *params: AbstractSweep | TogetherSweep | ParamMeasT | Sequence[ParamMeasT], -) -> tuple[ - list[AbstractSweep | TogetherSweep], list[ParamMeasT | Sequence[ParamMeasT]] -]: - """ - Parse supplied arguments into sweep objects and measurement parameters - and their callables. - """ - sweep_instances: list[AbstractSweep | TogetherSweep] = [] - params_meas: list[ParamMeasT | Sequence[ParamMeasT]] = [] - for par in params: - if isinstance(par, (AbstractSweep, TogetherSweep)): - sweep_instances.append(par) - else: - params_meas.append(par) - return sweep_instances, params_meas +__all__ = ["DondKWargs", "ParameterGroup", "ParameterSetEvent", "dond"] diff --git a/src/qcodes/dataset/dond/do_nd_utils.py b/src/qcodes/dataset/dond/do_nd_utils.py index 5d0339c23b43..9c615932513f 100644 --- a/src/qcodes/dataset/dond/do_nd_utils.py +++ b/src/qcodes/dataset/dond/do_nd_utils.py @@ -1,145 +1,25 @@ -from __future__ import annotations - -import logging -from collections.abc import Callable, Generator, Sequence -from contextlib import contextmanager -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - import matplotlib.axes - import matplotlib.colorbar - - from qcodes.dataset.measurements import Measurement - -from qcodes.dataset.data_set_protocol import DataSetProtocol -from qcodes.dataset.plotting import plot_and_save_image -from qcodes.parameters import MultiParameter, ParameterBase - -if TYPE_CHECKING: - from qcodes.dataset.descriptions.versioning.rundescribertypes import Shapes - -log = logging.getLogger(__name__) - -ActionsT = Sequence[Callable[[], None]] -BreakConditionT = Callable[[], bool] - -ParamMeasT = ParameterBase | Callable[[], None] - -AxesTuple = tuple["matplotlib.axes.Axes", "matplotlib.colorbar.Colorbar"] -AxesTupleList = tuple[ - list["matplotlib.axes.Axes"], list["matplotlib.colorbar.Colorbar | None"] +from qcodes.dataset._dond.do_nd_utils import ( + ActionsT, + AxesTuple, + AxesTupleList, + AxesTupleListWithDataSet, + BreakConditionInterrupt, + BreakConditionT, + MeasInterruptT, + MultiAxesTupleListWithDataSet, + ParamMeasT, + catch_interrupts, +) + +__all__ = [ + "ActionsT", + "AxesTuple", + "AxesTupleList", + "AxesTupleListWithDataSet", + "BreakConditionInterrupt", + "BreakConditionT", + "MeasInterruptT", + "MultiAxesTupleListWithDataSet", + "ParamMeasT", + "catch_interrupts", ] -AxesTupleListWithDataSet = tuple[ - DataSetProtocol, - tuple["matplotlib.axes.Axes | None", ...], - tuple["matplotlib.colorbar.Colorbar | None", ...], -] -MultiAxesTupleListWithDataSet = tuple[ - tuple[DataSetProtocol, ...], - tuple[tuple["matplotlib.axes.Axes | None", ...], ...], - tuple[tuple["matplotlib.colorbar.Colorbar | None", ...], ...], -] - - -class BreakConditionInterrupt(Exception): - pass - - -MeasInterruptT = KeyboardInterrupt | BreakConditionInterrupt | None - - -def _register_parameters( - meas: Measurement, - param_meas: Sequence[ParamMeasT], - setpoints: Sequence[ParameterBase] | None = None, - shapes: Shapes | None = None, -) -> None: - real_parameters = [ - param for param in param_meas if isinstance(param, ParameterBase) - ] - - for parameter in real_parameters: - meas.register_parameter(parameter, setpoints=setpoints) - - if shapes is not None: - parameter_names = [param.register_name for param in real_parameters] - for param in real_parameters: - if isinstance(param, MultiParameter): - parameter_names.extend(param.full_names) - - filtered_shapes = { - name: shape for name, shape in shapes.items() if name in parameter_names - } - meas.set_shapes(shapes=filtered_shapes) - - -def _set_write_period(meas: Measurement, write_period: float | None = None) -> None: - if write_period is not None: - meas.write_period = write_period - - -def _handle_plotting( - data: DataSetProtocol, - do_plot: bool = True, - interrupted: MeasInterruptT = None, -) -> AxesTupleListWithDataSet: - """ - Save the plots created by datasaver as pdf and png - - Args: - data: a dataset to generate plots from - as plot. - do_plot: Should a plot be produced - interrupted: If the measurement was interrupted, this will be the exception. - - """ - res: AxesTupleListWithDataSet - if do_plot: - res = plot_and_save_image(data) - else: - res = data, (None,), (None,) - - if interrupted: - log.warning( - f"Measurement has been interrupted, data may be incomplete: {interrupted}" - ) - - return res - - -def _register_actions( - meas: Measurement, enter_actions: ActionsT, exit_actions: ActionsT -) -> None: - for action in enter_actions: - # this omits the possibility of passing - # argument to enter and exit actions. - # Do we want that? - meas.add_before_run(action, ()) - for action in exit_actions: - meas.add_after_run(action, ()) - - -@contextmanager -def catch_interrupts() -> Generator[Callable[[], MeasInterruptT | None], None, None]: - interrupt_exception: MeasInterruptT | None = None - interrupt_raised = False - - def get_interrupt_exception() -> MeasInterruptT | None: - nonlocal interrupt_exception - return interrupt_exception - - try: - yield get_interrupt_exception - except KeyboardInterrupt as e: - interrupt_exception = e - interrupt_raised = True - raise # Re-raise KeyboardInterrupt - except BreakConditionInterrupt as e: - interrupt_exception = e - interrupt_raised = True - # Don't re-raise BreakConditionInterrupt - finally: - if interrupt_raised: - log.warning( - f"Measurement has been interrupted, data may be incomplete: {interrupt_exception}" - ) diff --git a/src/qcodes/dataset/dond/sweeps.py b/src/qcodes/dataset/dond/sweeps.py index a998b4474c45..a5604c2c3b93 100644 --- a/src/qcodes/dataset/dond/sweeps.py +++ b/src/qcodes/dataset/dond/sweeps.py @@ -1,276 +1,9 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any - -import numpy as np -import numpy.typing as npt - -if TYPE_CHECKING: - from collections.abc import Iterable, Sequence - - from qcodes.dataset.dond.do_nd_utils import ActionsT - from qcodes.parameters import ParameterBase - - -class AbstractSweep[T: np.generic](ABC): - """ - Abstract sweep class that defines an interface for concrete sweep classes. - """ - - @abstractmethod - def get_setpoints(self) -> npt.NDArray[T]: - """ - Returns an array of setpoint values for this sweep. - """ - - @property - @abstractmethod - def param(self) -> ParameterBase: - """ - Returns the Qcodes sweep parameter. - """ - - @property - @abstractmethod - def delay(self) -> float: - """ - Delay between two consecutive sweep points. - """ - - @property - @abstractmethod - def num_points(self) -> int: - """ - Number of sweep points. - """ - - @property - @abstractmethod - def post_actions(self) -> ActionsT: - """ - Actions to be performed after setting param to its setpoint. - """ - - @property - def get_after_set(self) -> bool: - """ - Should we perform a call to get on the parameter after setting it - and store that rather than the setpoint value in the dataset? - - This defaults to False for backwards compatibility - but subclasses should overwrite this to implement if correctly. - """ - return False - - -class LinSweep(AbstractSweep[np.floating]): - """ - Linear sweep. - - Args: - param: Qcodes parameter to sweep. - start: Sweep start value. - stop: Sweep end value. - num_points: Number of sweep points. - delay: Time in seconds between two consecutive sweep points. - post_actions: Actions to do after each sweep point. - get_after_set: Should we perform a get on the parameter after setting it - and store the value returned by get rather than the set value in the dataset. - - """ - - def __init__( - self, - param: ParameterBase, - start: float, - stop: float, - num_points: int, - delay: float = 0, - post_actions: ActionsT = (), - get_after_set: bool = False, - ): - self._param = param - self._start = start - self._stop = stop - self._num_points = num_points - self._delay = delay - self._post_actions = post_actions - self._get_after_set = get_after_set - - def get_setpoints(self) -> npt.NDArray[np.floating]: - """ - Linear (evenly spaced) numpy array for supplied start, stop and - num_points. - """ - return np.linspace(self._start, self._stop, self._num_points) - - @property - def param(self) -> ParameterBase: - return self._param - - @property - def delay(self) -> float: - return self._delay - - @property - def num_points(self) -> int: - return self._num_points - - @property - def post_actions(self) -> ActionsT: - return self._post_actions - - @property - def get_after_set(self) -> bool: - return self._get_after_set - - -class LogSweep(AbstractSweep[np.floating]): - """ - Logarithmic sweep. - - Args: - param: Qcodes parameter for sweep. - start: Sweep start value. - stop: Sweep end value. - num_points: Number of sweep points. - delay: Time in seconds between two consecutive sweep points. - post_actions: Actions to do after each sweep point. - get_after_set: Should we perform a get on the parameter after setting it - and store the value returned by get rather than the set value in the dataset. - - """ - - def __init__( - self, - param: ParameterBase, - start: float, - stop: float, - num_points: int, - delay: float = 0, - post_actions: ActionsT = (), - get_after_set: bool = False, - ): - self._param = param - self._start = start - self._stop = stop - self._num_points = num_points - self._delay = delay - self._post_actions = post_actions - self._get_after_set = get_after_set - - def get_setpoints(self) -> npt.NDArray[np.floating]: - """ - Logarithmically spaced numpy array for supplied start, stop and - num_points. - """ - return np.logspace(self._start, self._stop, self._num_points) - - @property - def param(self) -> ParameterBase: - return self._param - - @property - def delay(self) -> float: - return self._delay - - @property - def num_points(self) -> int: - return self._num_points - - @property - def post_actions(self) -> ActionsT: - return self._post_actions - - @property - def get_after_set(self) -> bool: - return self._get_after_set - - -class ArraySweep[T: np.generic](AbstractSweep): - """ - Sweep the values of a given array. - - Args: - param: Qcodes parameter for sweep. - array: array with values to sweep. - delay: Time in seconds between two consecutive sweep points. - post_actions: Actions to do after each sweep point. - get_after_set: Should we perform a get on the parameter after setting it - and store the value returned by get rather than the set value in the dataset. - - """ - - def __init__( - self, - param: ParameterBase, - array: Sequence[Any] | npt.NDArray[T], - delay: float = 0, - post_actions: ActionsT = (), - get_after_set: bool = False, - ): - self._param = param - self._array = np.array(array) - self._delay = delay - self._post_actions = post_actions - self._get_after_set = get_after_set - - def get_setpoints(self) -> npt.NDArray[T]: - return self._array - - @property - def param(self) -> ParameterBase: - return self._param - - @property - def delay(self) -> float: - return self._delay - - @property - def num_points(self) -> int: - return len(self._array) - - @property - def post_actions(self) -> ActionsT: - return self._post_actions - - @property - def get_after_set(self) -> bool: - return self._get_after_set - - -class TogetherSweep: - """ - A combination of Multiple sweeps that are to be performed in parallel - such that all parameters in the `TogetherSweep` are set to the next value - before a parameter is read. - - """ - - def __init__(self, *sweeps: AbstractSweep): - if len(sweeps) == 0: - raise ValueError("A TogetherSweep must contain at least one sweep.") - - len_0 = sweeps[0].num_points - - for sweep in sweeps: - if sweep.num_points != len_0: - raise ValueError( - f"All Sweeps in a TogetherSweep must have the same length." - f"Sweep of {sweep.param} had {sweep.num_points} but the " - f"first one had {len_0}." - ) - - self._sweeps = tuple(sweeps) - - @property - def sweeps(self) -> tuple[AbstractSweep, ...]: - return self._sweeps - - def get_setpoints(self) -> Iterable: - return zip(*(sweep.get_setpoints() for sweep in self.sweeps)) - - @property - def num_points(self) -> int: - return self.sweeps[0].num_points +from qcodes.dataset._dond.sweeps import ( + AbstractSweep, + ArraySweep, + LinSweep, + LogSweep, + TogetherSweep, +) + +__all__ = ["AbstractSweep", "ArraySweep", "LinSweep", "LogSweep", "TogetherSweep"] diff --git a/src/qcodes/dataset/measurement_extensions.py b/src/qcodes/dataset/measurement_extensions.py index c11e2fc3d903..58add9384a59 100644 --- a/src/qcodes/dataset/measurement_extensions.py +++ b/src/qcodes/dataset/measurement_extensions.py @@ -8,9 +8,9 @@ from opentelemetry import trace -from qcodes.dataset.dond.do_nd import _Sweeper -from qcodes.dataset.dond.do_nd_utils import ParamMeasT, catch_interrupts -from qcodes.dataset.dond.sweeps import AbstractSweep, LinSweep, TogetherSweep +from qcodes.dataset._dond.do_nd import _Sweeper +from qcodes.dataset._dond.do_nd_utils import ParamMeasT, catch_interrupts +from qcodes.dataset._dond.sweeps import AbstractSweep, LinSweep, TogetherSweep from qcodes.dataset.measurements import DataSaver, Measurement from qcodes.dataset.threading import process_params_meas from qcodes.parameters.parameter_base import ParameterBase diff --git a/src/qcodes/instrument_drivers/basel/BaselSP983.py b/src/qcodes/instrument_drivers/basel/BaselSP983.py index 5ff7db48de7e..298302b5b878 100644 --- a/src/qcodes/instrument_drivers/basel/BaselSP983.py +++ b/src/qcodes/instrument_drivers/basel/BaselSP983.py @@ -1,90 +1,6 @@ -from typing import TYPE_CHECKING, Self +from qcodes.instrument_drivers.basel._basel_sp983 import BaselSP983 +from qcodes.utils.deprecate import _warn_deprecated_module -from qcodes.instrument import Instrument, InstrumentBaseKWArgs -from qcodes.parameters import DelegateParameter, Parameter -from qcodes.validators import Enum, Numbers +_warn_deprecated_module(__name__, "qcodes.instrument_drivers.basel") -if TYPE_CHECKING: - from typing import Unpack - - -class BaselSP983(Instrument): - """ - A virtual driver for the Basel SP 983 current to voltage converter. - - This driver supports both the SP 983 and SP 983c models. These differ only - in their handling of input offset voltage. It is the responsibility of the - user to capture the input offset, (from the voltage supply) and compensate - that as needed for SP 983. For SP 983c model, 'input_offset_voltage' - argument can be used to set up offset (This doesn't work for SP 983c01 - model). - - Note that, as this is a purely virtual driver, there is no support - for the remote control interface (SP 983a). It is the responsibility of - the user to ensure that values set here are in accordance with the values - set on the instrument. - - Args: - name - input_offset_voltage: (Optional) A source input offset voltage - parameter. The range for input is -10 to 10 Volts and it is - user's responsibility to ensure this. This source parameter is - used to set offset voltage parameter of the preamp and the - source parameter should represent a voltage source that is - connected to the "Offset Input Voltage" connector of the SP983C. - **kwargs: Forwarded to base class. - - """ - - def __init__( - self, - name: str, - input_offset_voltage: Parameter | None = None, - **kwargs: "Unpack[InstrumentBaseKWArgs]", - ): - super().__init__(name, **kwargs) - - self.gain: Parameter[float, Self] = self.add_parameter( - "gain", - initial_value=1e8, - label="Gain", - unit="V/A", - get_cmd=None, - set_cmd=None, - vals=Enum(1e09, 1e08, 1e07, 1e06, 1e05), - ) - """Parameter gain""" - - self.fcut: Parameter[float, Self] = self.add_parameter( - "fcut", - initial_value=1e3, - label="Cutoff frequency", - unit="Hz", - get_cmd=None, - set_cmd=None, - vals=Enum(30.0, 100.0, 300.0, 1e3, 3e3, 10e3, 30e3, 100e3, 1e6), - ) - """Parameter fcut""" - - self.offset_voltage: DelegateParameter = self.add_parameter( - "offset_voltage", - label="Offset Voltage", - unit="V", - vals=Numbers(-0.1, 0.1), - scale=100, - source=input_offset_voltage, - parameter_class=DelegateParameter, - ) - """Parameter offset_voltage""" - - def get_idn(self) -> dict[str, str | None]: - vendor = "Physics Basel" - model = "SP 983" - serial = None - firmware = None - return { - "vendor": vendor, - "model": model, - "serial": serial, - "firmware": firmware, - } +__all__ = ["BaselSP983"] diff --git a/src/qcodes/instrument_drivers/basel/BaselSP983a.py b/src/qcodes/instrument_drivers/basel/BaselSP983a.py index c85211fc2509..0d6627c94a4b 100644 --- a/src/qcodes/instrument_drivers/basel/BaselSP983a.py +++ b/src/qcodes/instrument_drivers/basel/BaselSP983a.py @@ -1,129 +1,6 @@ -from typing import TYPE_CHECKING, Self +from qcodes.instrument_drivers.basel._basel_sp983a import BaselSP983a +from qcodes.utils.deprecate import _warn_deprecated_module -import numpy as np +_warn_deprecated_module(__name__, "qcodes.instrument_drivers.basel") -from qcodes import validators as vals -from qcodes.instrument import VisaInstrument, VisaInstrumentKWArgs -from qcodes.parameters import DelegateParameter, Parameter - -if TYPE_CHECKING: - from typing import Unpack - - -class BaselSP983a(VisaInstrument): - """ - A driver for Basel Preamp's (SP983a) Remote Instrument - Model SP983a. - - Args: - name: name for your instrument driver instance - address: address of the connected remote controller of basel preamp - input_offset_voltage: (Optional) A source input offset voltage - parameter. The range for input is -10 to 10 Volts and it is - user's responsibility to ensure this. This source parameter is - used to set offset voltage parameter of the preamp and the - source parameter should represent a voltage source that is - connected to the "Offset Input Voltage" connector of the SP983C. - **kwargs: Forwarded to base class. - - """ - - default_terminator = "\r\n" - - def __init__( - self, - name: str, - address: str, - input_offset_voltage: Parameter | None = None, - **kwargs: "Unpack[VisaInstrumentKWArgs]", - ) -> None: - super().__init__(name, address, **kwargs) - - self.connect_message() - - self.gain: Parameter[float, Self] = self.add_parameter( - "gain", - label="Gain", - unit="V/A", - set_cmd=self._set_gain, - get_cmd=self._get_gain, - vals=vals.Enum(1e5, 1e6, 1e7, 1e8, 1e9), - ) - """Parameter gain""" - self.fcut: Parameter[float, Self] = self.add_parameter( - "fcut", - unit="Hz", - label="Filter Cut-Off Frequency", - get_cmd=self._get_filter, - get_parser=self._parse_filter_value, - set_cmd=self._set_filter, - val_mapping={ - 30: "30", - 100: "100", - 300: "300", - 1000: "1k", - 3000: "3k", - 10e3: "10k", - 30e3: "30k", - 100e3: "100k", - 1e6: "FULL", - }, - ) - """Parameter fcut""" - self.overload_status: Parameter[str, Self] = self.add_parameter( - "overload_status", label="Overload Status", set_cmd=False, get_cmd="GET O" - ) - """Parameter overload_status""" - - self.offset_voltage: DelegateParameter = self.add_parameter( - "offset_voltage", - label="Offset Voltage for SP983C", - unit="V", - vals=vals.Numbers(-0.1, 0.1), - scale=100, - source=input_offset_voltage, - parameter_class=DelegateParameter, - ) - """Parameter offset_voltage""" - - def get_idn(self) -> dict[str, str | None]: - vendor = "Physics Basel" - model = "SP 983A" - serial = None - firmware = None - return { - "vendor": vendor, - "model": model, - "serial": serial, - "firmware": firmware, - } - - def _set_gain(self, value: float) -> None: - r = self.ask(f"SET G 1E{int(np.log10(value))}") - if r != "OK": - raise ValueError(f"Expected OK return but got: {r}") - - def _get_gain(self) -> float: - s = self.ask("GET G") - r = s.split("Gain: ")[1] - return float(r) - - def _set_filter(self, value: str) -> None: - r = self.ask(f"SET F {value}") - if r != "OK": - raise ValueError(f"Expected OK return but got: {r}") - - def _get_filter(self) -> str: - s = self.ask("GET F") - return s.split("Filter: ")[1] - - @staticmethod - def _parse_filter_value(val: str) -> str: - if val.startswith("F"): - return "FULL" - elif val[-3::] == "kHz": - return str(int(val[0:-3])) + "k" - - elif val[-2::] == "Hz": - return str(int(val[0:-2])) - else: - raise ValueError(f"Could not interpret result. Got: {val}") +__all__ = ["BaselSP983a"] diff --git a/src/qcodes/instrument_drivers/basel/BaselSP983c.py b/src/qcodes/instrument_drivers/basel/BaselSP983c.py index 64a16e09ec42..c7d7b7a04f0a 100644 --- a/src/qcodes/instrument_drivers/basel/BaselSP983c.py +++ b/src/qcodes/instrument_drivers/basel/BaselSP983c.py @@ -1,41 +1,6 @@ -from .BaselSP983 import BaselSP983 +from qcodes.instrument_drivers.basel._basel_sp983c import BaselSP983c +from qcodes.utils.deprecate import _warn_deprecated_module +_warn_deprecated_module(__name__, "qcodes.instrument_drivers.basel") -class BaselSP983c(BaselSP983): - """ - A virtual driver for the Basel SP 983c current to voltage converter. - - This driver supports both the SP 983 and SP 983c models. These differ only - in their handling of input offset voltage. It is the responsibility of the - user to capture the input offset, (from the voltage supply) and compensate - that as needed for SP 983. For SP 983c model, 'input_offset_voltage' - argument can be used to set up offset (This doesn't work for SP 983c01 - model). - - Note that, as this is a purely virtual driver, there is no support - for the remote control interface (SP 983a). It is the responsibility of - the user to ensure that values set here are in accordance with the values - set on the instrument. - - Args: - name - input_offset_voltage: (Optional) A source input offset voltage - parameter. The range for input is -10 to 10 Volts and it is - user's responsibility to ensure this. This source parameter is - used to set offset voltage parameter of the preamp and the - source parameter should represent a voltage source that is - connected to the "Offset Input Voltage" connector of the SP983C. - - """ - - def get_idn(self) -> dict[str, str | None]: - vendor = "Physics Basel" - model = "SP 983c" - serial = None - firmware = None - return { - "vendor": vendor, - "model": model, - "serial": serial, - "firmware": firmware, - } +__all__ = ["BaselSP983c"] diff --git a/src/qcodes/instrument_drivers/basel/__init__.py b/src/qcodes/instrument_drivers/basel/__init__.py index d884d23d2b5d..24ccb9a602a0 100644 --- a/src/qcodes/instrument_drivers/basel/__init__.py +++ b/src/qcodes/instrument_drivers/basel/__init__.py @@ -1,5 +1,12 @@ -from .BaselSP983 import BaselSP983 -from .BaselSP983a import BaselSP983a -from .BaselSP983c import BaselSP983c +from qcodes.utils.deprecate import _keep_attributes_shadowing_submodules + +from ._basel_sp983 import BaselSP983 +from ._basel_sp983a import BaselSP983a +from ._basel_sp983c import BaselSP983c + +# Keep the public objects when the deprecated module aliases that share +# their names are imported. +_keep_attributes_shadowing_submodules(__name__) + __all__ = ["BaselSP983", "BaselSP983a", "BaselSP983c"] diff --git a/src/qcodes/instrument_drivers/basel/_basel_sp983.py b/src/qcodes/instrument_drivers/basel/_basel_sp983.py new file mode 100644 index 000000000000..5ff7db48de7e --- /dev/null +++ b/src/qcodes/instrument_drivers/basel/_basel_sp983.py @@ -0,0 +1,90 @@ +from typing import TYPE_CHECKING, Self + +from qcodes.instrument import Instrument, InstrumentBaseKWArgs +from qcodes.parameters import DelegateParameter, Parameter +from qcodes.validators import Enum, Numbers + +if TYPE_CHECKING: + from typing import Unpack + + +class BaselSP983(Instrument): + """ + A virtual driver for the Basel SP 983 current to voltage converter. + + This driver supports both the SP 983 and SP 983c models. These differ only + in their handling of input offset voltage. It is the responsibility of the + user to capture the input offset, (from the voltage supply) and compensate + that as needed for SP 983. For SP 983c model, 'input_offset_voltage' + argument can be used to set up offset (This doesn't work for SP 983c01 + model). + + Note that, as this is a purely virtual driver, there is no support + for the remote control interface (SP 983a). It is the responsibility of + the user to ensure that values set here are in accordance with the values + set on the instrument. + + Args: + name + input_offset_voltage: (Optional) A source input offset voltage + parameter. The range for input is -10 to 10 Volts and it is + user's responsibility to ensure this. This source parameter is + used to set offset voltage parameter of the preamp and the + source parameter should represent a voltage source that is + connected to the "Offset Input Voltage" connector of the SP983C. + **kwargs: Forwarded to base class. + + """ + + def __init__( + self, + name: str, + input_offset_voltage: Parameter | None = None, + **kwargs: "Unpack[InstrumentBaseKWArgs]", + ): + super().__init__(name, **kwargs) + + self.gain: Parameter[float, Self] = self.add_parameter( + "gain", + initial_value=1e8, + label="Gain", + unit="V/A", + get_cmd=None, + set_cmd=None, + vals=Enum(1e09, 1e08, 1e07, 1e06, 1e05), + ) + """Parameter gain""" + + self.fcut: Parameter[float, Self] = self.add_parameter( + "fcut", + initial_value=1e3, + label="Cutoff frequency", + unit="Hz", + get_cmd=None, + set_cmd=None, + vals=Enum(30.0, 100.0, 300.0, 1e3, 3e3, 10e3, 30e3, 100e3, 1e6), + ) + """Parameter fcut""" + + self.offset_voltage: DelegateParameter = self.add_parameter( + "offset_voltage", + label="Offset Voltage", + unit="V", + vals=Numbers(-0.1, 0.1), + scale=100, + source=input_offset_voltage, + parameter_class=DelegateParameter, + ) + """Parameter offset_voltage""" + + def get_idn(self) -> dict[str, str | None]: + vendor = "Physics Basel" + model = "SP 983" + serial = None + firmware = None + return { + "vendor": vendor, + "model": model, + "serial": serial, + "firmware": firmware, + } diff --git a/src/qcodes/instrument_drivers/basel/_basel_sp983a.py b/src/qcodes/instrument_drivers/basel/_basel_sp983a.py new file mode 100644 index 000000000000..c85211fc2509 --- /dev/null +++ b/src/qcodes/instrument_drivers/basel/_basel_sp983a.py @@ -0,0 +1,129 @@ +from typing import TYPE_CHECKING, Self + +import numpy as np + +from qcodes import validators as vals +from qcodes.instrument import VisaInstrument, VisaInstrumentKWArgs +from qcodes.parameters import DelegateParameter, Parameter + +if TYPE_CHECKING: + from typing import Unpack + + +class BaselSP983a(VisaInstrument): + """ + A driver for Basel Preamp's (SP983a) Remote Instrument - Model SP983a. + + Args: + name: name for your instrument driver instance + address: address of the connected remote controller of basel preamp + input_offset_voltage: (Optional) A source input offset voltage + parameter. The range for input is -10 to 10 Volts and it is + user's responsibility to ensure this. This source parameter is + used to set offset voltage parameter of the preamp and the + source parameter should represent a voltage source that is + connected to the "Offset Input Voltage" connector of the SP983C. + **kwargs: Forwarded to base class. + + """ + + default_terminator = "\r\n" + + def __init__( + self, + name: str, + address: str, + input_offset_voltage: Parameter | None = None, + **kwargs: "Unpack[VisaInstrumentKWArgs]", + ) -> None: + super().__init__(name, address, **kwargs) + + self.connect_message() + + self.gain: Parameter[float, Self] = self.add_parameter( + "gain", + label="Gain", + unit="V/A", + set_cmd=self._set_gain, + get_cmd=self._get_gain, + vals=vals.Enum(1e5, 1e6, 1e7, 1e8, 1e9), + ) + """Parameter gain""" + self.fcut: Parameter[float, Self] = self.add_parameter( + "fcut", + unit="Hz", + label="Filter Cut-Off Frequency", + get_cmd=self._get_filter, + get_parser=self._parse_filter_value, + set_cmd=self._set_filter, + val_mapping={ + 30: "30", + 100: "100", + 300: "300", + 1000: "1k", + 3000: "3k", + 10e3: "10k", + 30e3: "30k", + 100e3: "100k", + 1e6: "FULL", + }, + ) + """Parameter fcut""" + self.overload_status: Parameter[str, Self] = self.add_parameter( + "overload_status", label="Overload Status", set_cmd=False, get_cmd="GET O" + ) + """Parameter overload_status""" + + self.offset_voltage: DelegateParameter = self.add_parameter( + "offset_voltage", + label="Offset Voltage for SP983C", + unit="V", + vals=vals.Numbers(-0.1, 0.1), + scale=100, + source=input_offset_voltage, + parameter_class=DelegateParameter, + ) + """Parameter offset_voltage""" + + def get_idn(self) -> dict[str, str | None]: + vendor = "Physics Basel" + model = "SP 983A" + serial = None + firmware = None + return { + "vendor": vendor, + "model": model, + "serial": serial, + "firmware": firmware, + } + + def _set_gain(self, value: float) -> None: + r = self.ask(f"SET G 1E{int(np.log10(value))}") + if r != "OK": + raise ValueError(f"Expected OK return but got: {r}") + + def _get_gain(self) -> float: + s = self.ask("GET G") + r = s.split("Gain: ")[1] + return float(r) + + def _set_filter(self, value: str) -> None: + r = self.ask(f"SET F {value}") + if r != "OK": + raise ValueError(f"Expected OK return but got: {r}") + + def _get_filter(self) -> str: + s = self.ask("GET F") + return s.split("Filter: ")[1] + + @staticmethod + def _parse_filter_value(val: str) -> str: + if val.startswith("F"): + return "FULL" + elif val[-3::] == "kHz": + return str(int(val[0:-3])) + "k" + + elif val[-2::] == "Hz": + return str(int(val[0:-2])) + else: + raise ValueError(f"Could not interpret result. Got: {val}") diff --git a/src/qcodes/instrument_drivers/basel/_basel_sp983c.py b/src/qcodes/instrument_drivers/basel/_basel_sp983c.py new file mode 100644 index 000000000000..ee7480243366 --- /dev/null +++ b/src/qcodes/instrument_drivers/basel/_basel_sp983c.py @@ -0,0 +1,41 @@ +from ._basel_sp983 import BaselSP983 + + +class BaselSP983c(BaselSP983): + """ + A virtual driver for the Basel SP 983c current to voltage converter. + + This driver supports both the SP 983 and SP 983c models. These differ only + in their handling of input offset voltage. It is the responsibility of the + user to capture the input offset, (from the voltage supply) and compensate + that as needed for SP 983. For SP 983c model, 'input_offset_voltage' + argument can be used to set up offset (This doesn't work for SP 983c01 + model). + + Note that, as this is a purely virtual driver, there is no support + for the remote control interface (SP 983a). It is the responsibility of + the user to ensure that values set here are in accordance with the values + set on the instrument. + + Args: + name + input_offset_voltage: (Optional) A source input offset voltage + parameter. The range for input is -10 to 10 Volts and it is + user's responsibility to ensure this. This source parameter is + used to set offset voltage parameter of the preamp and the + source parameter should represent a voltage source that is + connected to the "Offset Input Voltage" connector of the SP983C. + + """ + + def get_idn(self) -> dict[str, str | None]: + vendor = "Physics Basel" + model = "SP 983c" + serial = None + firmware = None + return { + "vendor": vendor, + "model": model, + "serial": serial, + "firmware": firmware, + } diff --git a/src/qcodes/instrument_drivers/stanford_research/SG384.py b/src/qcodes/instrument_drivers/stanford_research/SG384.py index d65181fbe4e7..7643dc3f9ac7 100644 --- a/src/qcodes/instrument_drivers/stanford_research/SG384.py +++ b/src/qcodes/instrument_drivers/stanford_research/SG384.py @@ -1,226 +1,6 @@ -from typing import TYPE_CHECKING, Self +from qcodes.instrument_drivers.stanford_research._sg384 import SG384 +from qcodes.utils.deprecate import _warn_deprecated_module -from qcodes import validators as vals -from qcodes.instrument import VisaInstrument, VisaInstrumentKWArgs +_warn_deprecated_module(__name__, "qcodes.instrument_drivers.stanford_research") -if TYPE_CHECKING: - from typing import Unpack - - from qcodes.parameters import Parameter - - -class SG384(VisaInstrument): - """ - QCoDeS driver for the Stanford Research Systems SG384 RF Signal Generator. - - Status: beta version - Includes the essential commands from the manual - """ - - default_terminator = "\n" - - def __init__( - self, - name: str, - address: str, - reset: bool = False, - **kwargs: "Unpack[VisaInstrumentKWArgs]", - ): - super().__init__(name, address, **kwargs) - # signal synthesis commands - self.frequency: Parameter[float, Self] = self.add_parameter( - name="frequency", - label="Frequency", - unit="Hz", - get_cmd="FREQ?", - set_cmd="FREQ {:.3f}", - get_parser=float, - vals=vals.Numbers(min_value=9.5e5, max_value=4.05e9), - ) - """Parameter frequency""" - self.phase: Parameter[float, Self] = self.add_parameter( - name="phase", - label="Carrier phase", - unit="deg", - get_cmd="PHAS?", - set_cmd="PHAS {:.1f}", - get_parser=float, - vals=vals.Numbers(min_value=-360, max_value=360), - ) - """Parameter phase""" - self.amplitude_LF: Parameter[float, Self] = self.add_parameter( - name="amplitude_LF", - label="Power of BNC output", - unit="dBm", - get_cmd="AMPL?", - set_cmd="AMPL {:.2f}", - get_parser=float, - vals=vals.Numbers(min_value=-47, max_value=13), - ) - """Parameter amplitude_LF""" - self.amplitude_RF: Parameter[float, Self] = self.add_parameter( - name="amplitude_RF", - label="Power of type-N RF output", - unit="dBm", - get_cmd="AMPR?", - set_cmd="AMPR {:.2f}", - get_parser=float, - vals=vals.Numbers(min_value=-110, max_value=16.5), - ) - """Parameter amplitude_RF""" - self.amplitude_HF: Parameter[float, Self] = self.add_parameter( - name="amplitude_HF", - label="Power of RF doubler output", - unit="dBm", - get_cmd="AMPH?", - set_cmd="AMPH {:.2f}", - get_parser=float, - vals=vals.Numbers(min_value=-10, max_value=13), - ) - """Parameter amplitude_HF""" - self.amplitude_clock: Parameter[float, Self] = self.add_parameter( - name="amplitude_clock", - label="Rear clock output", - unit="Vpp", - get_cmd="AMPC?", - set_cmd="AMPC {:.2f}", - get_parser=float, - vals=vals.Numbers(min_value=0.4, max_value=1.00), - ) - """Parameter amplitude_clock""" - self.noise_mode: Parameter[str, Self] = self.add_parameter( - name="noise_mode", - label="RF PLL loop filter mode", - get_cmd="NOIS?", - set_cmd="NOIS {}", - val_mapping={"Mode 1": 0, "Mode 2": 1}, - ) - """Parameter noise_mode""" - self.enable_RF: Parameter[str, Self] = self.add_parameter( - name="enable_RF", - label="Type-N RF output", - get_cmd="ENBR?", - set_cmd="ENBR {}", - val_mapping={"OFF": 0, "ON": 1}, - ) - """Parameter enable_RF""" - self.enable_LF: Parameter[str, Self] = self.add_parameter( - name="enable_LF", - label="BNC output", - get_cmd="ENBL?", - set_cmd="ENBL {}", - val_mapping={"OFF": 0, "ON": 1}, - ) - """Parameter enable_LF""" - self.enable_HF: Parameter[str, Self] = self.add_parameter( - name="enable_HF", - label="RF doubler output", - get_cmd="ENBH?", - set_cmd="ENBH {}", - val_mapping={"OFF": 0, "ON": 1}, - ) - """Parameter enable_HF""" - self.enable_clock: Parameter[str, Self] = self.add_parameter( - name="enable_clock", - label="Rear clock output", - get_cmd="ENBC?", - set_cmd="ENBC {}", - val_mapping={"OFF": 0, "ON": 1}, - ) - """Parameter enable_clock""" - self.offset_clock: Parameter[float, Self] = self.add_parameter( - name="offset_clock", - label="Rear clock offset voltage", - unit="V", - get_cmd="OFSC?", - set_cmd="OFSC {}", - get_parser=float, - vals=vals.Numbers(min_value=-2, max_value=2), - ) - """Parameter offset_clock""" - self.offset_rearDC: Parameter[float, Self] = self.add_parameter( - name="offset_rearDC", - label="Rear DC offset voltage", - unit="V", - get_cmd="OFSD?", - set_cmd="OFSD {}", - get_parser=float, - vals=vals.Numbers(min_value=-10, max_value=10), - ) - """Parameter offset_rearDC""" - self.offset_bnc: Parameter[float, Self] = self.add_parameter( - name="offset_bnc", - label="Low frequency BNC output", - unit="V", - get_cmd="OFSL?", - set_cmd="OFSL {}", - get_parser=float, - vals=vals.Numbers(min_value=-1.5, max_value=1.5), - ) - """Parameter offset_bnc""" - # Modulation commands - self.modulation_coupling: Parameter[str, Self] = self.add_parameter( - name="modulation_coupling", - label="External modulation input coupling", - get_cmd="COUP?", - set_cmd="COUP {}", - val_mapping={"AC": 0, "DC": 1}, - ) - """Parameter modulation_coupling""" - self.FM_deviation: Parameter[float, Self] = self.add_parameter( - name="FM_deviation", - label="Frequency modulation deviation", - unit="Hz", - get_cmd="FDEV?", - set_cmd="FDEV {:.1f}", - get_parser=float, - vals=vals.Numbers(min_value=0.1, max_value=32e6), - ) - """Parameter FM_deviation""" - self.modulation_function: Parameter[str, Self] = self.add_parameter( - name="modulation_function", - label="Modulation function for AM/FM/PhiM", - get_cmd="MFNC?", - set_cmd="MFNC {}", - val_mapping={ - "Sine": 0, - "Ramp": 1, - "Triangle": 2, - "Square": 3, - "Noise": 4, - "External": 5, - }, - ) - """Parameter modulation_function""" - self.enable_modulation: Parameter[str, Self] = self.add_parameter( - name="enable_modulation", - get_cmd="MODL?", - set_cmd="MODL {}", - val_mapping={"OFF": 0, "ON": 1}, - ) - """Parameter enable_modulation""" - self.modulation_rate: Parameter[float, Self] = self.add_parameter( - name="modulation_rate", - get_cmd="RATE?", - set_cmd="RATE {:.6f}", - get_parser=float, - vals=vals.Numbers(min_value=1e-6, max_value=50e3), - ) - """Parameter modulation_rate""" - self.modulation_type: Parameter[str, Self] = self.add_parameter( - name="modulation_type", - label="Current modulation type", - get_cmd="TYPE?", - set_cmd="TYPE {}", - val_mapping={ - "AM": 0, - "FM": 1, - "Phi": 2, - "Sweep": 3, - "Pulse": 4, - "Blank": 5, - "IQ": 6, - }, - ) - """Parameter modulation_type""" - self.connect_message() +__all__ = ["SG384"] diff --git a/src/qcodes/instrument_drivers/stanford_research/SR560.py b/src/qcodes/instrument_drivers/stanford_research/SR560.py index 57f9cea85174..fa49913ad1d1 100644 --- a/src/qcodes/instrument_drivers/stanford_research/SR560.py +++ b/src/qcodes/instrument_drivers/stanford_research/SR560.py @@ -1,183 +1,11 @@ -from typing import TYPE_CHECKING, Self +from qcodes.instrument_drivers.stanford_research._sr560 import ( + SR560, + SR560VoltageParameter, +) +from qcodes.utils.deprecate import _warn_deprecated_module -from qcodes.instrument import Instrument, InstrumentBaseKWArgs -from qcodes.parameters import MultiParameter, Parameter, ParamRawDataType -from qcodes.validators import Bool, Enum +_warn_deprecated_module(__name__, "qcodes.instrument_drivers.stanford_research") -if TYPE_CHECKING: - from typing import Unpack +VoltageParameter = SR560VoltageParameter - -class VoltageParameter(MultiParameter): - """ - Amplified voltage measurement via an SR560 preamp and a measured voltage. - - To be used when you feed a voltage into an SR560, send the SR560's - output voltage to a lockin or other voltage amplifier, and you have - the voltage reading from that amplifier as a qcodes parameter. - - ``VoltageParameter.get()`` returns ``(voltage_raw, voltage)`` - - Args: - measured_param: a gettable parameter returning the - voltage read from the SR560 output. - v_amp_ins: an SR560 instance where you manually - maintain the present settings of the real SR560 amp. - - Note: it should be possible to use other voltage preamps, if they - define parameters ``gain`` (V_out / V_in) and ``invert`` - (bool, output is inverted) - - name: the name of the current output. Default 'volt'. - Also used as the name of the whole parameter. - - """ - - def __init__( - self, - measured_param: Parameter, - v_amp_ins: "SR560", - name: str = "volt", - snapshot_value: bool = True, - ): - p_name = measured_param.name - - super().__init__( - name=name, - names=(p_name + "_raw", name), - shapes=((), ()), - instrument=v_amp_ins, - ) - - self._measured_param = measured_param - - p_label = getattr(measured_param, "label", "") - p_unit = getattr(measured_param, "unit", "") - - self.labels = (p_label, "Voltage") - self.units = (p_unit, "V") - - def get_raw(self) -> tuple[ParamRawDataType, ParamRawDataType]: - volt = self._measured_param.get() - assert isinstance(self.instrument, SR560) - volt_amp = volt / self.instrument.gain.get() - - if self.instrument.invert.get(): - volt_amp *= -1 - - value = (volt, volt_amp) - return value - - -class SR560(Instrument): - """ - QCoDeS driver for the Stanford Research Systems SR560 Voltage-preamplifier. - - This is a virtual driver only and will not talk to your instrument. - - Note: - - The ``cutoff_lo`` and ``cutoff_hi`` parameters will interact with - each other on the instrument (hi cannot be <= lo) but this is not - managed here, you must ensure yourself that both are correct whenever - you change one of them. - - ``gain`` has a vernier setting, which does not yield a well-defined - output. We restrict this driver to only the predefined gain values. - - """ - - def __init__(self, name: str, **kwargs: "Unpack[InstrumentBaseKWArgs]"): - super().__init__(name, **kwargs) - - cutoffs = [ - "DC", - 0.03, - 0.1, - 0.3, - 1, - 3, - 10, - 30, - 100, - 300, - 1000, - 3000, - 10000, - 30000, - 100000, - 300000, - 1000000, - ] - - gains = [ - 1, - 2, - 5, - 10, - 20, - 50, - 100, - 200, - 500, - 1000, - 2000, - 5000, - 10000, - 20000, - 50000, - ] - - self.cutoff_lo: Parameter = self.add_parameter( - "cutoff_lo", - get_cmd=None, - set_cmd=None, - initial_value="DC", - label="High pass", - unit="Hz", - vals=Enum(*cutoffs), - ) - """Parameter cutoff_lo""" - - self.cutoff_hi: Parameter = self.add_parameter( - "cutoff_hi", - get_cmd=None, - set_cmd=None, - initial_value=1e6, - label="Low pass", - unit="Hz", - vals=Enum(*cutoffs), - ) - """Parameter cutoff_hi""" - - self.invert: Parameter[bool, Self] = self.add_parameter( - "invert", - get_cmd=None, - set_cmd=None, - initial_value=True, - label="Inverted output", - vals=Bool(), - ) - """Parameter invert""" - - self.gain: Parameter = self.add_parameter( - "gain", - get_cmd=None, - set_cmd=None, - initial_value=10, - label="Gain", - unit=None, - vals=Enum(*gains), - ) - """Parameter gain""" - - def get_idn(self) -> dict[str, str | None]: - vendor = "Stanford Research Systems" - model = "SR560" - serial = None - firmware = None - - return { - "vendor": vendor, - "model": model, - "serial": serial, - "firmware": firmware, - } +__all__ = ["SR560", "VoltageParameter"] diff --git a/src/qcodes/instrument_drivers/stanford_research/SR830.py b/src/qcodes/instrument_drivers/stanford_research/SR830.py index fb8b5c7fbe74..a169a422f2f6 100644 --- a/src/qcodes/instrument_drivers/stanford_research/SR830.py +++ b/src/qcodes/instrument_drivers/stanford_research/SR830.py @@ -1,1034 +1,15 @@ -from __future__ import annotations - -import time -from functools import partial -from typing import TYPE_CHECKING, Any, ClassVar, Self - -import numpy as np - -from qcodes.instrument import VisaInstrument, VisaInstrumentKWArgs -from qcodes.parameters import ( - ArrayParameter, - Parameter, - ParameterWithSetpoints, - ParamRawDataType, +from qcodes.instrument_drivers.stanford_research._sr830 import ( + SR830, + SR830ChannelBuffer, + SR830ChannelTrace, + SR830GeneratedSetPoints, ) -from qcodes.validators import Arrays, ComplexNumbers, Enum, Ints, Numbers, Strings - -if TYPE_CHECKING: - from collections.abc import Iterable - from typing import Unpack - - -class ChannelTrace(ParameterWithSetpoints): - """ - Parameter class for the two channel buffers - """ - - def __init__(self, name: str, channel: int, **kwargs: Any) -> None: - """ - Args: - name: The name of the parameter - channel: The relevant channel (1 or 2). The name should - match this. - **kwargs: kwargs are forwarded to base class. - - """ - super().__init__(name, **kwargs) - - self._valid_channels = (1, 2) - - if channel not in self._valid_channels: - raise ValueError( - "Invalid channel specifier. SR830 only has channels 1 and 2." - ) - - if not isinstance(self.root_instrument, SR830): - raise ValueError( # noqa: TRY004 - "Invalid parent instrument. ChannelBuffer can only live on an SR830." - ) - - self.channel = channel - self.update_unit() - - def update_unit(self) -> None: - assert isinstance(self.root_instrument, SR830) - params = self.root_instrument.parameters - if params[f"ch{self.channel}_ratio"].get() != "none": - self.unit = "%" - else: - disp = params[f"ch{self.channel}_display"].get() - if disp == "Phase": - self.unit = "deg" - else: - self.unit = "V" - self.label = disp - - def get_raw(self) -> ParamRawDataType: - """ - Get command. Returns numpy array - """ - assert isinstance(self.root_instrument, SR830) - N = self.root_instrument.buffer_npts() - if N == 0: - raise ValueError( - "No points stored in SR830 data buffer. Can not poll anything." - ) - - # poll raw binary data - self.root_instrument.write(f"TRCL ? {self.channel}, 0, {N}") - rawdata = self.root_instrument.visa_handle.read_raw() - - # parse it - realdata = np.frombuffer(rawdata, dtype=" None: - """ - Args: - name: The name of the parameter - instrument: The parent instrument - channel: The relevant channel (1 or 2). The name should - should match this. - - """ - self._valid_channels = (1, 2) - - if channel not in self._valid_channels: - raise ValueError( - "Invalid channel specifier. SR830 only has channels 1 and 2." - ) - - if not isinstance(instrument, SR830): - raise ValueError( # noqa: TRY004 - "Invalid parent instrument. ChannelBuffer can only live on an SR830." - ) - - super().__init__( - name, - shape=(1,), # dummy initial shape - unit="V", # dummy initial unit - setpoint_names=("Time",), - setpoint_labels=("Time",), - setpoint_units=("s",), - docstring="Holds an acquired (part of the) data buffer of one channel.", - instrument=instrument, - ) - - self.channel = channel - - def prepare_buffer_readout(self) -> None: - """ - Function to generate the setpoints for the channel buffer and - get the right units - """ - assert isinstance(self.instrument, SR830) - N = self.instrument.buffer_npts() # problem if this is zero? - # TODO (WilliamHPNielsen): what if SR was changed during acquisition? - SR = self.instrument.buffer_SR() - if SR == "Trigger": - self.setpoint_units = ("",) - self.setpoint_names = ("trig_events",) - self.setpoint_labels = ("Trigger event number",) - self.setpoints = (tuple(np.arange(0, N)),) - else: - dt = 1 / SR - self.setpoint_units = ("s",) - self.setpoint_names = ("Time",) - self.setpoint_labels = ("Time",) - self.setpoints = (tuple(np.linspace(0, N * dt, N)),) - - self.shape = (N,) - - params = self.instrument.parameters - # YES, it should be: comparing to the string 'none' and not - # the None literal - if params[f"ch{self.channel}_ratio"].get() != "none": - self.unit = "%" - else: - disp = params[f"ch{self.channel}_display"].get() - if disp == "Phase": - self.unit = "deg" - else: - self.unit = "V" - - if self.channel == 1: - self.instrument._buffer1_ready = True - else: - self.instrument._buffer2_ready = True - - def get_raw(self) -> ParamRawDataType: - """ - Get command. Returns numpy array - """ - assert isinstance(self.instrument, SR830) - if self.channel == 1: - ready = self.instrument._buffer1_ready - else: - ready = self.instrument._buffer2_ready - - if not ready: - raise RuntimeError("Buffer not ready. Please run prepare_buffer_readout") - N = self.instrument.buffer_npts() - if N == 0: - raise ValueError( - "No points stored in SR830 data buffer. Can not poll anything." - ) - - # poll raw binary data - self.instrument.write(f"TRCL ? {self.channel}, 0, {N}") - rawdata = self.instrument.visa_handle.read_raw() - - # parse it - realdata = np.frombuffer(rawdata, dtype=" tuple[float, int]: - parts = s.split(",") - - return float(parts[0]), int(parts[1]) - - # TODO: Parameters that can be set with multiple arguments - # For the OEXP command for example two arguments are needed - self.X_offset: Parameter[tuple[float, int], Self] = self.add_parameter( - "X_offset", get_cmd="OEXP? 1", get_parser=parse_offset_get - ) - """Parameter X_offset""" - - self.Y_offset: Parameter[tuple[float, int], Self] = self.add_parameter( - "Y_offset", get_cmd="OEXP? 2", get_parser=parse_offset_get - ) - """Parameter Y_offset""" - - self.R_offset: Parameter[tuple[float, int], Self] = self.add_parameter( - "R_offset", get_cmd="OEXP? 3", get_parser=parse_offset_get - ) - """Parameter R_offset""" - - # Aux input/output - for i in [1, 2, 3, 4]: - self.add_parameter( - f"aux_in{i}", - label=f"Aux input {i}", - get_cmd=f"OAUX? {i}", - get_parser=float, - unit="V", - ) - - self.add_parameter( - f"aux_out{i}", - label=f"Aux output {i}", - get_cmd=f"AUXV? {i}", - get_parser=float, - set_cmd=f"AUXV {i}, {{}}", - unit="V", - ) - - # Setup - self.output_interface: Parameter[str, Self] = self.add_parameter( - "output_interface", - label="Output interface", - get_cmd="OUTX?", - set_cmd="OUTX {}", - val_mapping={ - "RS232": "0\n", - "GPIB": "1\n", - }, - ) - """Parameter output_interface""" - - # Data transfer - self.X: Parameter[float, Self] = self.add_parameter( - "X", get_cmd="OUTP? 1", get_parser=float, unit="V" - ) - """Parameter X""" - - self.Y: Parameter[float, Self] = self.add_parameter( - "Y", get_cmd="OUTP? 2", get_parser=float, unit="V" - ) - """Parameter Y""" - - self.R: Parameter[float, Self] = self.add_parameter( - "R", get_cmd="OUTP? 3", get_parser=float, unit="V" - ) - """Parameter R""" - - self.P: Parameter[float, Self] = self.add_parameter( - "P", get_cmd="OUTP? 4", get_parser=float, unit="deg" - ) - """Parameter P""" - - self.complex_voltage: Parameter = self.add_parameter( - "complex_voltage", - label="Voltage", - get_cmd=self._get_complex_voltage, - unit="V", - docstring="Complex voltage parameter " - "calculated from X, Y phase using " - "Z = X +j*Y", - vals=ComplexNumbers(), - ) - """Complex voltage parameter calculated from X, Y phase using Z = X +j*Y""" - - # Data buffer settings - self.buffer_SR: Parameter = self.add_parameter( - "buffer_SR", - label="Buffer sample rate", - get_cmd="SRAT ?", - set_cmd=self._set_buffer_SR, - unit="Hz", - val_mapping={ - 62.5e-3: 0, - 0.125: 1, - 0.250: 2, - 0.5: 3, - 1: 4, - 2: 5, - 4: 6, - 8: 7, - 16: 8, - 32: 9, - 64: 10, - 128: 11, - 256: 12, - 512: 13, - "Trigger": 14, - }, - get_parser=int, - ) - """Parameter buffer_SR""" - - self.buffer_acq_mode: Parameter[str, Self] = self.add_parameter( - "buffer_acq_mode", - label="Buffer acquistion mode", - get_cmd="SEND ?", - set_cmd="SEND {}", - val_mapping={"single shot": 0, "loop": 1}, - get_parser=int, - ) - """Parameter buffer_acq_mode""" - - self.buffer_trig_mode: Parameter[str, Self] = self.add_parameter( - "buffer_trig_mode", - label="Buffer trigger start mode", - get_cmd="TSTR ?", - set_cmd="TSTR {}", - val_mapping={"ON": 1, "OFF": 0}, - get_parser=int, - ) - """Parameter buffer_trig_mode""" - - self.buffer_npts: Parameter[int, Self] = self.add_parameter( - "buffer_npts", - label="Buffer number of stored points", - get_cmd="SPTS ?", - get_parser=int, - ) - """Parameter buffer_npts""" - - self.sweep_setpoints: GeneratedSetPoints = self.add_parameter( - "sweep_setpoints", - parameter_class=GeneratedSetPoints, - vals=Arrays(shape=(self.buffer_npts.get,)), - ) - """Parameter sweep_setpoints""" - - # Channel setup - for ch in range(1, 3): - # detailed validation and mapping performed in set/get functions - self.add_parameter( - f"ch{ch}_ratio", - label=f"Channel {ch} ratio", - get_cmd=partial(self._get_ch_ratio, ch), - set_cmd=partial(self._set_ch_ratio, ch), - vals=Strings(), - ) - self.add_parameter( - f"ch{ch}_display", - label=f"Channel {ch} display", - get_cmd=partial(self._get_ch_display, ch), - set_cmd=partial(self._set_ch_display, ch), - vals=Strings(), - ) - self.add_parameter( - f"ch{ch}_databuffer", channel=ch, parameter_class=ChannelBuffer - ) - self.add_parameter( - f"ch{ch}_datatrace", - channel=ch, - vals=Arrays(shape=(self.buffer_npts.get,)), - setpoints=(self.sweep_setpoints,), - parameter_class=ChannelTrace, - ) - - # Auto functions - self.add_function("auto_gain", call_cmd="AGAN") - self.add_function("auto_reserve", call_cmd="ARSV") - self.add_function("auto_phase", call_cmd="APHS") - self.add_function("auto_offset", call_cmd="AOFF {0}", args=[Enum(1, 2, 3)]) - - # Interface - self.add_function("reset", call_cmd="*RST") - - self.add_function("disable_front_panel", call_cmd="OVRM 0") - self.add_function("enable_front_panel", call_cmd="OVRM 1") - - self.add_function( - "send_trigger", - call_cmd="TRIG", - docstring=( - "Send a software trigger. " - "This command has the same effect as a " - "trigger at the rear panel trigger" - " input." - ), - ) - - self.add_function( - "buffer_start", - call_cmd="STRT", - docstring=( - "The buffer_start command starts or " - "resumes data storage. buffer_start" - " is ignored if storage is already in" - " progress." - ), - ) - - self.add_function( - "buffer_pause", - call_cmd="PAUS", - docstring=( - "The buffer_pause command pauses data " - "storage. If storage is already paused " - "or reset then this command is ignored." - ), - ) - - self.add_function( - "buffer_reset", - call_cmd="REST", - docstring=( - "The buffer_reset command resets the data" - " buffers. The buffer_reset command can " - "be sent at any time - any storage in " - "progress, paused or not, will be reset." - " This command will erase the data " - "buffer." - ), - ) - - # Initialize the proper units of the outputs and sensitivities - self.input_config() - - # start keeping track of buffer setpoints - self._buffer1_ready = False - self._buffer2_ready = False - - self.connect_message() - - SNAP_PARAMETERS: ClassVar[dict[str, str]] = { - "x": "1", - "y": "2", - "r": "3", - "p": "4", - "phase": "4", - "θ": "4", - "aux1": "5", - "aux2": "6", - "aux3": "7", - "aux4": "8", - "freq": "9", - "ch1": "10", - "ch2": "11", - } - - def snap(self, *parameters: str) -> tuple[float, ...]: - """ - Get between 2 and 6 parameters at a single instant. This provides a - coherent snapshot of measured signals. Pick up to 6 from: X, Y, R, θ, - the aux inputs 1-4, frequency, or what is currently displayed on - channels 1 and 2. - - Reading X and Y (or R and θ) gives a coherent snapshot of the signal. - Snap is important when the time constant is very short, a time constant - less than 100 ms. - - Args: - *parameters: From 2 to 6 strings of names of parameters for which - the values are requested. including: 'x', 'y', 'r', 'p', - 'phase' or 'θ', 'aux1', 'aux2', 'aux3', 'aux4', 'freq', - 'ch1', and 'ch2'. - - Returns: - A tuple of floating point values in the same order as requested. - - Examples: - >>> lockin.snap('x','y') -> tuple(x,y) - - >>> lockin.snap('aux1','aux2','freq','phase') - >>> -> tuple(aux1,aux2,freq,phase) - - Note: - Volts for x, y, r, and aux 1-4 - Degrees for θ - Hertz for freq - Unknown for ch1 and ch2. It will depend on what was set. - - - If X,Y,R and θ are all read, then the values of X,Y are recorded - approximately 10 µs apart from R,θ. Thus, the values of X and Y - may not yield the exact values of R and θ from a single snap. - - The values of the Aux Inputs may have an uncertainty of - up to 32 µs. - - The frequency is computed only every other period or 40 ms, - whichever is longer. - - """ - if not 2 <= len(parameters) <= 6: - raise KeyError( - "It is only possible to request values of 2 to 6 parameters at a time." - ) - - for name in parameters: - if name.lower() not in self.SNAP_PARAMETERS: - raise KeyError( - f"{name} is an unknown parameter. Refer" - f" to `SNAP_PARAMETERS` for a list of valid" - f" parameter names" - ) - - p_ids = [self.SNAP_PARAMETERS[name.lower()] for name in parameters] - output = self.ask(f"SNAP? {','.join(p_ids)}") - - return tuple(float(val) for val in output.split(",")) - - def increment_sensitivity(self) -> bool: - """ - Increment the sensitivity setting of the lock-in. This is equivalent - to pushing the sensitivity up button on the front panel. This has no - effect if the sensitivity is already at the maximum. - - Returns: - Whether or not the sensitivity was actually changed. - - """ - return self._change_sensitivity(1) - - def decrement_sensitivity(self) -> bool: - """ - Decrement the sensitivity setting of the lock-in. This is equivalent - to pushing the sensitivity down button on the front panel. This has no - effect if the sensitivity is already at the minimum. - - Returns: - Whether or not the sensitivity was actually changed. - - """ - return self._change_sensitivity(-1) - - def _change_sensitivity(self, dn: int) -> bool: - if self.input_config() in ["a", "a-b"]: - n_to = self._N_TO_VOLT - to_n = self._VOLT_TO_N - else: - n_to = self._N_TO_CURR - to_n = self._CURR_TO_N - - n = to_n[self.sensitivity()] - - if n + dn > max(n_to.keys()) or n + dn < min(n_to.keys()): - return False - - self.sensitivity.set(n_to[n + dn]) - return True - - def _set_buffer_SR(self, SR: int) -> None: - self.write(f"SRAT {SR}") - self._buffer1_ready = False - self._buffer2_ready = False - self.sweep_setpoints.update_units_if_constant_sample_rate() - - def _get_ch_ratio(self, channel: int) -> str: - val_mapping = { - 1: {0: "none", 1: "Aux In 1", 2: "Aux In 2"}, - 2: {0: "none", 1: "Aux In 3", 2: "Aux In 4"}, - } - resp = int(self.ask(f"DDEF ? {channel}").split(",")[1]) - - return val_mapping[channel][resp] - - def _set_ch_ratio(self, channel: int, ratio: str) -> None: - val_mapping = { - 1: {"none": 0, "Aux In 1": 1, "Aux In 2": 2}, - 2: {"none": 0, "Aux In 3": 1, "Aux In 4": 2}, - } - vals = val_mapping[channel].keys() - if ratio not in vals: - raise ValueError(f"{ratio} not in {vals}") - ratio_int = val_mapping[channel][ratio] - disp_val = int(self.ask(f"DDEF ? {channel}").split(",")[0]) - self.write(f"DDEF {channel}, {disp_val}, {ratio_int}") - self._buffer_ready = False - - def _get_ch_display(self, channel: int) -> str: - val_mapping = { - 1: {0: "X", 1: "R", 2: "X Noise", 3: "Aux In 1", 4: "Aux In 2"}, - 2: {0: "Y", 1: "Phase", 2: "Y Noise", 3: "Aux In 3", 4: "Aux In 4"}, - } - resp = int(self.ask(f"DDEF ? {channel}").split(",")[0]) - - return val_mapping[channel][resp] - - def _set_ch_display(self, channel: int, disp: str) -> None: - val_mapping = { - 1: {"X": 0, "R": 1, "X Noise": 2, "Aux In 1": 3, "Aux In 2": 4}, - 2: {"Y": 0, "Phase": 1, "Y Noise": 2, "Aux In 3": 3, "Aux In 4": 4}, - } - vals = val_mapping[channel].keys() - if disp not in vals: - raise ValueError(f"{disp} not in {vals}") - disp_int = val_mapping[channel][disp] - # Since ratio AND display are set simultaneously, - # we get and then re-set the current ratio value - ratio_val = int(self.ask(f"DDEF ? {channel}").split(",")[1]) - self.write(f"DDEF {channel}, {disp_int}, {ratio_val}") - self._buffer_ready = False - # we update the unit of the datatrace - # according to the choice of channel - params = self.parameters - dataparam = params[f"ch{channel}_datatrace"] - assert isinstance(dataparam, ChannelTrace) - dataparam.update_unit() - - def _set_units(self, unit: str) -> None: - # TODO: - # make a public parameter function that allows to change the units - for param in [self.X, self.Y, self.R, self.sensitivity]: - param.unit = unit - - def _get_complex_voltage(self) -> complex: - x, y = self.snap("X", "Y") - return x + 1.0j * y - - def _get_input_config(self, s: int) -> str: - mode = self._N_TO_INPUT_CONFIG[int(s)] - - if mode in ["a", "a-b"]: - self.sensitivity.vals = self._VOLT_ENUM - self._set_units("V") - else: - self.sensitivity.vals = self._CURR_ENUM - self._set_units("A") - - return mode - - def _set_input_config(self, s: str) -> int: - if s in ["a", "a-b"]: - self.sensitivity.vals = self._VOLT_ENUM - self._set_units("V") - else: - self.sensitivity.vals = self._CURR_ENUM - self._set_units("A") - - return self._INPUT_CONFIG_TO_N[s] - - def _get_sensitivity(self, s: int) -> float: - if self.input_config() in ["a", "a-b"]: - return self._N_TO_VOLT[int(s)] - else: - return self._N_TO_CURR[int(s)] - - def _set_sensitivity(self, s: float) -> int: - if self.input_config() in ["a", "a-b"]: - return self._VOLT_TO_N[s] - else: - return self._CURR_TO_N[s] - - def autorange(self, max_changes: int = 1) -> None: - """ - Automatically changes the sensitivity of the instrument according to - the R value and defined max_changes. - - Args: - max_changes: Maximum number of steps allowing the function to - automatically change the sensitivity (default is 1). The actual - number of steps needed to change to the optimal sensitivity may - be more or less than this maximum. - - """ - - def autorange_once() -> bool: - r = self.R() - sens = self.sensitivity() - if r > 0.9 * sens: - return self.increment_sensitivity() - elif r < 0.1 * sens: - return self.decrement_sensitivity() - return False - - sets = 0 - while autorange_once() and sets < max_changes: - sets += 1 - time.sleep(self.time_constant()) - - def set_sweep_parameters( - self, - sweep_param: Parameter, - start: float, - stop: float, - n_points: int = 10, - label: str | None = None, - ) -> None: - self.sweep_setpoints.sweep_array = np.linspace(start, stop, n_points) - self.sweep_setpoints.unit = sweep_param.unit - if label is not None: - self.sweep_setpoints.label = label - elif sweep_param.label is not None: - self.sweep_setpoints.label = sweep_param.label - - -class GeneratedSetPoints(Parameter): - """ - A parameter that generates a setpoint array from start, stop and num points - parameters. - """ - - def __init__( - self, - sweep_array: Iterable[float | int] = np.linspace(0, 1, 10), - *args: Any, - **kwargs: Any, - ) -> None: - super().__init__(*args, **kwargs) - self.sweep_array = sweep_array - self.update_units_if_constant_sample_rate() - - def update_units_if_constant_sample_rate(self) -> None: - """ - If the buffer is filled at a constant sample rate, - update the unit to "s" and label to "Time"; - otherwise do nothing - """ - assert isinstance(self.root_instrument, SR830) - SR = self.root_instrument.buffer_SR.get() - if SR != "Trigger": - self.unit = "s" - self.label = "Time" +from qcodes.utils.deprecate import _warn_deprecated_module - def set_raw(self, value: Iterable[float | int]) -> None: - self.sweep_array = value +_warn_deprecated_module(__name__, "qcodes.instrument_drivers.stanford_research") - def get_raw(self) -> ParamRawDataType: - assert isinstance(self.root_instrument, SR830) - SR = self.root_instrument.buffer_SR.get() - if SR == "Trigger": - return self.sweep_array - else: - N = self.root_instrument.buffer_npts.get() - dt = 1 / SR +ChannelBuffer = SR830ChannelBuffer +ChannelTrace = SR830ChannelTrace +GeneratedSetPoints = SR830GeneratedSetPoints - return np.linspace(0, N * dt, N) +__all__ = ["SR830", "ChannelBuffer", "ChannelTrace", "GeneratedSetPoints"] diff --git a/src/qcodes/instrument_drivers/stanford_research/SR860.py b/src/qcodes/instrument_drivers/stanford_research/SR860.py index c20f52025d31..97621cc22f0b 100644 --- a/src/qcodes/instrument_drivers/stanford_research/SR860.py +++ b/src/qcodes/instrument_drivers/stanford_research/SR860.py @@ -1,25 +1,6 @@ -from typing import TYPE_CHECKING +from qcodes.instrument_drivers.stanford_research._sr860 import SR860 +from qcodes.utils.deprecate import _warn_deprecated_module -from qcodes.instrument_drivers.stanford_research.SR86x import SR86x +_warn_deprecated_module(__name__, "qcodes.instrument_drivers.stanford_research") -if TYPE_CHECKING: - from typing import Unpack - - from qcodes.instrument import VisaInstrumentKWArgs - - -class SR860(SR86x): - """ - QCoDeS driver for the Stanford Research Systems SR860 Lock-in Amplifier. - - The SR860 instrument is almost equal to the SR865, except for the max frequency - """ - - def __init__( - self, - name: str, - address: str, - reset: bool = False, - **kwargs: "Unpack[VisaInstrumentKWArgs]", - ) -> None: - super().__init__(name, address, max_frequency=500e3, reset=reset, **kwargs) +__all__ = ["SR860"] diff --git a/src/qcodes/instrument_drivers/stanford_research/SR865.py b/src/qcodes/instrument_drivers/stanford_research/SR865.py index cdf779883091..f08b48268189 100644 --- a/src/qcodes/instrument_drivers/stanford_research/SR865.py +++ b/src/qcodes/instrument_drivers/stanford_research/SR865.py @@ -1,25 +1,6 @@ -from typing import TYPE_CHECKING +from qcodes.instrument_drivers.stanford_research._sr865 import SR865 +from qcodes.utils.deprecate import _warn_deprecated_module -from qcodes.instrument_drivers.stanford_research.SR86x import SR86x +_warn_deprecated_module(__name__, "qcodes.instrument_drivers.stanford_research") -if TYPE_CHECKING: - from typing import Unpack - - from qcodes.instrument import VisaInstrumentKWArgs - - -class SR865(SR86x): - """ - QCoDeS driver for the Stanford Research Systems SR865 Lock-in Amplifier. - - The SR865 instrument is almost equal to the SR860, except for the max frequency - """ - - def __init__( - self, - name: str, - address: str, - reset: bool = False, - **kwargs: "Unpack[VisaInstrumentKWArgs]", - ) -> None: - super().__init__(name, address, max_frequency=2e6, reset=reset, **kwargs) +__all__ = ["SR865"] diff --git a/src/qcodes/instrument_drivers/stanford_research/SR865A.py b/src/qcodes/instrument_drivers/stanford_research/SR865A.py index 961070531710..265757381b37 100644 --- a/src/qcodes/instrument_drivers/stanford_research/SR865A.py +++ b/src/qcodes/instrument_drivers/stanford_research/SR865A.py @@ -1,26 +1,6 @@ -from typing import TYPE_CHECKING +from qcodes.instrument_drivers.stanford_research._sr865a import SR865A +from qcodes.utils.deprecate import _warn_deprecated_module -from qcodes.instrument_drivers.stanford_research.SR86x import SR86x +_warn_deprecated_module(__name__, "qcodes.instrument_drivers.stanford_research") -if TYPE_CHECKING: - from typing import Unpack - - from qcodes.instrument import VisaInstrumentKWArgs - - -class SR865A(SR86x): - """ - QCoDeS driver for the Stanford Research Systems SR865A Lock-in Amplifier. - - The SR865A instrument is almost equal to the SR865, except for the - max frequency - """ - - def __init__( - self, - name: str, - address: str, - reset: bool = False, - **kwargs: "Unpack[VisaInstrumentKWArgs]", - ) -> None: - super().__init__(name, address, max_frequency=4e6, reset=reset, **kwargs) +__all__ = ["SR865A"] diff --git a/src/qcodes/instrument_drivers/stanford_research/SR86x.py b/src/qcodes/instrument_drivers/stanford_research/SR86x.py index d2bec07c386f..a34d4ca1070b 100644 --- a/src/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/src/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -1,1321 +1,11 @@ -from __future__ import annotations - -import logging -from typing import TYPE_CHECKING, Any, ClassVar, Self - -import numpy as np -import numpy.typing as npt - -from qcodes.instrument import ( - ChannelList, - ChannelTuple, - InstrumentBaseKWArgs, - InstrumentChannel, - VisaInstrument, - VisaInstrumentKWArgs, +from qcodes.instrument_drivers.stanford_research._sr86x import ( + SR86x, + SR86xBuffer, + SR86xBufferReadout, + SR86xDataChannel, ) -from qcodes.parameters import ArrayParameter -from qcodes.validators import ComplexNumbers, Enum, Ints, Numbers - -if TYPE_CHECKING: - from collections.abc import Callable, Sequence - from typing import Unpack - - from qcodes.metadatable import SnapshotUpdate - from qcodes.parameters import Parameter - -log = logging.getLogger(__name__) - - -class SR86xBufferReadout(ArrayParameter): - """ - The parameter array that holds read out data. We need this to be compatible - with qcodes.Measure - - Args: - name: Name of the parameter. - instrument: The instrument to add this parameter to. - - """ - - def __init__(self, name: str, instrument: SR86x, **kwargs: Any) -> None: - unit = "deg" - if name in ["X", "Y", "R"]: - unit = "V" - - super().__init__( - name, - shape=(1,), # dummy initial shape - unit=unit, - setpoint_names=("Time",), - setpoint_labels=("Time",), - setpoint_units=("s",), - instrument=instrument, - docstring="Holds an acquired (part of the) data buffer of one channel.", - **kwargs, - ) - - self._capture_data: npt.NDArray | None = None - - def prepare_readout(self, capture_data: npt.NDArray) -> None: - """ - Prepare this parameter for readout. - - Args: - capture_data: The data to capture. - - """ - self._capture_data = capture_data - - data_len = len(capture_data) - self.shape = (data_len,) - self.setpoint_units = ("",) - self.setpoint_names = ("sample_nr",) - self.setpoint_labels = ("Sample number",) - self.setpoints = (tuple(np.arange(0, data_len)),) - - def get_raw(self) -> npt.NDArray: - """ - Public method to access the capture data - """ - if self._capture_data is None: - raise ValueError( - f"Cannot return data for parameter {self.name}. " - f"Please prepare for readout by calling " - f"'get_capture_data' with appropriate " - f"configuration settings" - ) - - return self._capture_data - - -class SR86xBuffer(InstrumentChannel["SR86x"]): - """ - Buffer module for the SR86x drivers. - - This driver has been verified to work with the SR860 and SR865. - For reference, please consult the SR860 - manual: http://thinksrs.com/downloads/PDFs/Manuals/SR860m.pdf - """ - - def __init__( - self, parent: SR86x, name: str, **kwargs: Unpack[InstrumentBaseKWArgs] - ) -> None: - super().__init__(parent, name, **kwargs) - - self.capture_length_in_kb: Parameter[int, Self] = self.add_parameter( - "capture_length_in_kb", - label="get/set capture length", - get_cmd="CAPTURELEN?", - set_cmd="CAPTURELEN {}", - set_parser=self._set_capture_len_parser, - get_parser=int, - unit="kB", - ) - """Parameter capture_length_in_kb""" - self.bytes_per_sample = 4 - self.min_capture_length_in_kb = 1 # i.e. minimum buffer size - self.max_capture_length_in_kb = 4096 # i.e. maximum buffer size - # Maximum amount of kB that can be read per single CAPTUREGET command - self.max_size_per_reading_in_kb = 64 - - self.capture_config: Parameter[str, Self] = self.add_parameter( - "capture_config", - label="capture configuration", - get_cmd="CAPTURECFG?", - set_cmd="CAPTURECFG {}", - val_mapping={"X": "0", "X,Y": "1", "R,T": "2", "X,Y,R,T": "3"}, - ) - """Parameter capture_config configures which parameters we want to capture""" - - self.capture_rate_max: Parameter[float, Self] = self.add_parameter( - "capture_rate_max", - label="capture rate maximum", - get_cmd="CAPTURERATEMAX?", - get_parser=float, - ) - """Parameter capture_rate_max""" - - self.capture_rate: Parameter[float, Self] = self.add_parameter( - "capture_rate", - label="capture rate raw", - get_cmd="CAPTURERATE?", - set_cmd="CAPTURERATE {}", - get_parser=float, - set_parser=self._set_capture_rate_parser, - ) - """Parameter capture_rate""" - - max_rate = self.capture_rate_max() - self.available_frequencies = [max_rate / 2**i for i in range(20)] - - self.capture_status: Parameter[str, Self] = self.add_parameter( - "capture_status", label="capture status", get_cmd="CAPTURESTAT?" - ) - """Parameter capture_status: Are we capturing at the moment?""" - - self.count_capture_bytes: Parameter[int, Self] = self.add_parameter( - "count_capture_bytes", - label="captured bytes", - get_cmd="CAPTUREBYTES?", - unit="B", - get_parser=int, - docstring="Number of bytes captured so far in the buffer. Can be " - "used to track live progress.", - ) - """Number of bytes captured so far in the buffer. Can be used to track live progress.""" - - self.count_capture_kilobytes: Parameter[int, Self] = self.add_parameter( - "count_capture_kilobytes", - label="captured kilobytes", - get_cmd="CAPTUREPROG?", - get_parser=int, - unit="kB", - docstring="Number of kilobytes captured so far in the buffer, " - "rounded-up to 2 kilobyte chunks. Capture must be " - "stopped before requesting the value of this " - "parameter. If the acquisition wrapped during operating " - "in Continuous mode, then the returned value is " - "simply equal to the current capture length.", - ) - """ - Number of kilobytes captured so far in the buffer, rounded-up to 2 kilobyte chunks. - Capture must be stopped before requesting the value of this parameter. - If the acquisition wrapped during operating in Continuous mode, - then the returned value is simply equal to the current capture length. - """ - - self.X: SR86xBufferReadout = self.add_parameter( - "X", parameter_class=SR86xBufferReadout - ) - """ - X buffer readout. - """ - self.Y: SR86xBufferReadout = self.add_parameter( - "Y", parameter_class=SR86xBufferReadout - ) - """ - Y buffer readout. - """ - self.R: SR86xBufferReadout = self.add_parameter( - "R", parameter_class=SR86xBufferReadout - ) - """ - R buffer readout. - """ - self.T: SR86xBufferReadout = self.add_parameter( - "T", parameter_class=SR86xBufferReadout - ) - """ - T buffer readout. - """ - - def snapshot_base( - self, - update: bool | SnapshotUpdate | None = "Only_invalid", - params_to_skip_update: Sequence[str] | None = None, - ) -> dict[Any, Any]: - if params_to_skip_update is None: - params_to_skip_update = [] - # we omit count_capture_kilobytes from the snapshot because - # it can only be read after a completed capture and will - # timeout otherwise when the snapshot is updated, e.g. at - # station creation time - params_to_skip_update = list(params_to_skip_update) - params_to_skip_update.append("count_capture_kilobytes") - - snapshot = super().snapshot_base(update, params_to_skip_update) - return snapshot - - def _set_capture_len_parser(self, capture_length_in_kb: int) -> int: - """ - Parse the capture length in kB according to the way buffer treats it - (refer to the manual for details). The given value has to fit in the - range and has to be even, otherwise this function raises exceptions. - - Args: - capture_length_in_kb: The desired capture length in kB. - - Returns: - capture_length_in_kb - - """ - if capture_length_in_kb % 2: - raise ValueError("The capture length should be an even number") - - if ( - not self.min_capture_length_in_kb - <= capture_length_in_kb - <= self.max_capture_length_in_kb - ): - raise ValueError( - f"The capture length should be between " - f"{self.min_capture_length_in_kb} and " - f"{self.max_capture_length_in_kb}" - ) - - return capture_length_in_kb - - def set_capture_rate_to_maximum(self) -> None: - """ - Sets the capture rate to maximum. The maximum capture rate is - retrieved from the device, and depends on the current value of the - time constant. - """ - self.capture_rate(self.capture_rate_max()) - - def _set_capture_rate_parser(self, capture_rate_hz: float) -> int: - """ - According to the manual, the capture rate query returns a value in - Hz, but then setting this value it is expected to give a value n, - where the capture rate in Hz is given by - capture_rate_hz = max_rate / 2 ** n. Please see page 136 of the - manual. Here n is an integer in the range [0, 20]. - - Args: - capture_rate_hz: The desired capture rate in Hz. If the desired - rate is more than 1 Hz from the nearest valid rate, a warning - is issued and the nearest valid rate it used. - - Returns: - n_round - - """ - max_rate = self.capture_rate_max() - n = np.log2(max_rate / capture_rate_hz) - n_round = round(n) - - if not 0 <= n_round <= 20: - raise ValueError( - f"The chosen frequency is invalid. Please " - f"consult the SR860 manual at page 136. " - f"The maximum capture rate is {max_rate}" - ) - - nearest_valid_rate = max_rate / 2**n_round - if abs(capture_rate_hz - nearest_valid_rate) > 1: - available_frequencies = ", ".join( - str(f) for f in self.available_frequencies - ) - log.warning(f"Warning: Setting capture rate to {nearest_valid_rate:.5} Hz") - log.warning(f"The available frequencies are: {available_frequencies}") - - return n_round - - def start_capture(self, acquisition_mode: str, trigger_mode: str) -> None: - """ - Start an acquisition. Please see page 137 of the manual for a detailed - explanation. - - Args: - acquisition_mode: "ONE" | "CONT" - trigger_mode: "IMM" | "TRIG" | "SAMP" - - """ - - if acquisition_mode not in ["ONE", "CONT"]: - raise ValueError("The acquisition mode needs to be either 'ONE' or 'CONT'") - - if trigger_mode not in ["IMM", "TRIG", "SAMP"]: - raise ValueError( - "The trigger mode needs to be either 'IMM', 'TRIG' or 'SAMP'" - ) - - cmd_str = f"CAPTURESTART {acquisition_mode}, {trigger_mode}" - self.write(cmd_str) - - def stop_capture(self) -> None: - """Stop a capture""" - self.write("CAPTURESTOP") - - def _get_list_of_capture_variable_names(self) -> list[str]: - """ - Retrieve the list of names of variables (readouts) that are - set to be captured - """ - return self.capture_config().split(",") - - def _get_number_of_capture_variables(self) -> int: - """ - Retrieve the number of variables (readouts) that are - set to be captured - """ - capture_variables = self._get_list_of_capture_variable_names() - n_variables = len(capture_variables) - return n_variables - - def _calc_capture_size_in_kb(self, sample_count: int) -> int: - """ - Given the number of samples to capture, calculate the capture length - that the buffer needs to be set to in order to fit the requested - number of samples. Note that the number of activated readouts is - taken into account. - """ - n_variables = self._get_number_of_capture_variables() - total_size_in_kb = int( - np.ceil(n_variables * sample_count * self.bytes_per_sample / 1024) - ) - # Make sure that the total size in kb is an even number, as expected by - # the instrument - if total_size_in_kb % 2: - total_size_in_kb += 1 - return total_size_in_kb - - def set_capture_length_to_fit_samples(self, sample_count: int) -> None: - """ - Set the capture length of the buffer to fit the given number of - samples. - - Args: - sample_count: Number of samples that the buffer has to fit - - """ - total_size_in_kb = self._calc_capture_size_in_kb(sample_count) - self.capture_length_in_kb(total_size_in_kb) - - def wait_until_samples_captured(self, sample_count: int) -> None: - """ - Wait until the given number of samples is captured. This function - is blocking and has to be used with caution because it does not have - a timeout. - - Args: - sample_count: Number of samples that needs to be captured - - """ - n_captured_bytes = 0 - n_variables = self._get_number_of_capture_variables() - n_bytes_to_capture = sample_count * n_variables * self.bytes_per_sample - while n_captured_bytes < n_bytes_to_capture: - n_captured_bytes = self.count_capture_bytes() - - def get_capture_data(self, sample_count: int) -> dict[str, npt.NDArray]: - """ - Read the given number of samples of the capture data from the buffer. - - Args: - sample_count: number of samples to read from the buffer - - Returns: - The keys in the dictionary correspond to the captured - variables. For instance, if before the capture, the capture - config was set as 'capture_config("X,Y")', then the keys will - be "X" and "Y". The values in the dictionary are numpy arrays - of numbers. - - """ - total_size_in_kb = self._calc_capture_size_in_kb(sample_count) - capture_variables = self._get_list_of_capture_variable_names() - n_variables = self._get_number_of_capture_variables() - - values = self._get_raw_capture_data(total_size_in_kb) - - # Remove zeros which mark the end part of the buffer that is not - # filled with captured data - values = values[values != 0] - - values = values.reshape((-1, n_variables)).T - values = values[:, :sample_count] - - data = {k: v for k, v in zip(capture_variables, values)} - - for capture_variable in capture_variables: - buffer_parameter = getattr(self, capture_variable) - buffer_parameter.prepare_readout(data[capture_variable]) - - return data - - def _get_raw_capture_data(self, size_in_kb: int) -> npt.NDArray: - """ - Read data from the buffer from its beginning avoiding the instrument - limit of 64 kilobytes per reading. - - Args: - size_in_kb :Size of the data that needs to be read; if it exceeds - the capture length, an exception is raised. - - Returns: - A one-dimensional numpy array of the requested data. Note that the - returned array contains data for all the variables that are - mentioned in the capture config. - - """ - current_capture_length = self.capture_length_in_kb() - if size_in_kb > current_capture_length: - raise ValueError( - f"The size of the requested data ({size_in_kb}kB) " - f"is larger than current capture length of the " - f"buffer ({current_capture_length}kB)." - ) - - values: npt.NDArray = np.array([]) - data_size_to_read_in_kb = size_in_kb - n_readings = 0 - - while data_size_to_read_in_kb > 0: - offset = n_readings * self.max_size_per_reading_in_kb - - if data_size_to_read_in_kb > self.max_size_per_reading_in_kb: - size_of_this_reading = self.max_size_per_reading_in_kb - else: - size_of_this_reading = data_size_to_read_in_kb - - data_from_this_reading = self._get_raw_capture_data_block( - size_of_this_reading, offset_in_kb=offset - ) - values = np.append(values, data_from_this_reading) - - data_size_to_read_in_kb -= size_of_this_reading - n_readings += 1 - - return values - - def _get_raw_capture_data_block( - self, size_in_kb: int, offset_in_kb: int = 0 - ) -> npt.NDArray: - """ - Read data from the buffer. The maximum amount of data that can be - read with this function (size_in_kb) is 64kB (this limitation comes - from the instrument). The offset argument can be used to navigate - along the buffer. - - An exception will be raised if either size_in_kb or offset_in_kb are - longer that the *current* capture length (number of kB of data that is - captured so far rounded up to 2kB chunks). If (offset_in_kb + - size_in_kb) is longer than the *current* capture length, - the instrument returns the wrapped data. - - For more information, refer to the description of the "CAPTUREGET" - command in the manual. - - Args: - size_in_kb: Amount of data in kB that is to be read from the buffer - offset_in_kb: Offset within the buffer of where to read the data; - for example, when 0 is specified, the data is read from the - start of the buffer. - - Returns: - A one-dimensional numpy array of the requested data. Note that the - returned array contains data for all the variables that are - mentioned in the capture config. - - """ - if size_in_kb > self.max_size_per_reading_in_kb: - raise ValueError( - f"The size of the requested data ({size_in_kb}kB) " - f"is larger than maximum size that can be read " - f"at once ({self.max_size_per_reading_in_kb}kB)." - ) - - # Calculate the size of the data captured so far, in kB, rounded up - # to 2kB chunks - size_of_currently_captured_data = int( - np.ceil(np.ceil(self.count_capture_bytes() / 1024) / 2) * 2 - ) - - if size_in_kb > size_of_currently_captured_data: - raise ValueError( - f"The size of the requested data ({size_in_kb}kB) " - f"cannot be larger than the size of currently " - f"captured data rounded up to 2kB chunks " - f"({size_of_currently_captured_data}kB)" - ) - - if offset_in_kb > size_of_currently_captured_data: - raise ValueError( - f"The offset for reading the requested data " - f"({offset_in_kb}kB) cannot be larger than the " - f"size of currently captured data rounded up to " - f"2kB chunks " - f"({size_of_currently_captured_data}kB)" - ) - - values = self.parent.visa_handle.query_binary_values( - f"CAPTUREGET? {offset_in_kb}, {size_in_kb}", - datatype="f", - is_big_endian=False, - expect_termination=False, - ) - # the sr86x does not include an extra termination char on binary - # messages so we set expect_termination to False - - return np.array(values) - - def capture_one_sample_per_trigger( - self, trigger_count: int, start_triggers_pulsetrain: Callable[..., Any] - ) -> dict[str, npt.NDArray]: - """ - Capture one sample per each trigger, and return when the specified - number of triggers has been received. - - Args: - trigger_count: Number of triggers to capture samples for - start_triggers_pulsetrain: By calling this *non-blocking* - function, the train of trigger pulses should start - - Returns: - The keys in the dictionary correspond to the captured - variables. For instance, if before the capture, the capture - config was set as 'capture_config("X,Y")', then the keys will - be "X" and "Y". The values in the dictionary are numpy arrays - of numbers. - - """ - self.set_capture_length_to_fit_samples(trigger_count) - self.start_capture("ONE", "SAMP") - start_triggers_pulsetrain() - self.wait_until_samples_captured(trigger_count) - self.stop_capture() - return self.get_capture_data(trigger_count) - - def capture_samples_after_trigger( - self, sample_count: int, send_trigger: Callable[..., Any] - ) -> dict[str, npt.NDArray]: - """ - Capture a number of samples after a trigger has been received. - Please refer to page 135 of the manual for details. - - Args: - sample_count: Number of samples to capture - send_trigger: By calling this *non-blocking* function, one trigger - should be sent that will initiate the capture - - Returns: - The keys in the dictionary correspond to the captured - variables. For instance, if before the capture, the capture - config was set as 'capture_config("X,Y")', then the keys will - be "X" and "Y". The values in the dictionary are numpy arrays - of numbers. - - """ - self.set_capture_length_to_fit_samples(sample_count) - self.start_capture("ONE", "TRIG") - send_trigger() - self.wait_until_samples_captured(sample_count) - self.stop_capture() - return self.get_capture_data(sample_count) - - def capture_samples(self, sample_count: int) -> dict[str, npt.NDArray]: - """ - Capture a number of samples at a capture rate, starting immediately. - Unlike the "continuous" capture mode, here the buffer does not get - overwritten with the new data once the buffer is full. - - The function blocks until the required number of samples is acquired, - and returns them. - - Args: - sample_count: Number of samples to capture - - Returns: - The keys in the dictionary correspond to the captured - variables. For instance, if before the capture, the capture - config was set as 'capture_config("X,Y")', then the keys will - be "X" and "Y". The values in the dictionary are numpy arrays - of numbers. - - """ - self.set_capture_length_to_fit_samples(sample_count) - self.start_capture("ONE", "IMM") - self.wait_until_samples_captured(sample_count) - self.stop_capture() - return self.get_capture_data(sample_count) - - -class SR86xDataChannel(InstrumentChannel["SR86x"]): - """ - Implements a data channel of SR86x lock-in amplifier. Parameters that are - assigned to these channels get plotted on the display of the instrument. - Moreover, there are commands that allow to conveniently retrieve the values - of the parameters that are currently assigned to the data channels. - - This class relies on the available parameter names that should be - mentioned in the lock-in amplifier class in `PARAMETER_NAMES` attribute. - - Args: - parent: an instance of SR86x driver - name: data channel name that is to be used to reference it from the - parent - cmd_id: this ID is used in VISA commands to refer to this data channel, - usually is an integer number - cmd_id_name: this name can also be used in VISA commands along with - channel_id; it is not used in this implementation, but is added - for reference - color: every data channel is also referred to by the color with which it - is being plotted on the instrument's screen; added here only for - reference - - """ - - def __init__( - self, - parent: SR86x, - name: str, - cmd_id: str, - cmd_id_name: str | None = None, - color: str | None = None, - **kwargs: Unpack[InstrumentBaseKWArgs], - ) -> None: - super().__init__(parent, name, **kwargs) - - self._cmd_id = cmd_id - self._cmd_id_name = cmd_id_name - self._color = color - - self.assigned_parameter: Parameter[str, Self] = self.add_parameter( - "assigned_parameter", - label=f"Data channel {cmd_id} parameter", - docstring=f"Allows to set and get the " - f"parameter that is assigned to data " - f"channel {cmd_id}", - set_cmd=f"CDSP {cmd_id}, {{}}", - get_cmd=f"CDSP? {cmd_id}", - val_mapping=self.parent.PARAMETER_NAMES, - ) - """Allows to set and get the parameter that is assigned to the channel""" - - @property - def cmd_id(self) -> str: - return self._cmd_id - - @property - def cmd_id_name(self) -> str | None: - return self._cmd_id_name - - @property - def color(self) -> str | None: - return self._color - - -class SR86x(VisaInstrument): - """ - Base class for Stanford SR86x Lock-in Amplifier drivers. This class should not - be instantiated directly instead one of the model specific sub classes should be used. - """ - - _VOLT_TO_N: ClassVar[dict[int | float, int]] = { - 1: 0, - 500e-3: 1, - 200e-3: 2, - 100e-3: 3, - 50e-3: 4, - 20e-3: 5, - 10e-3: 6, - 5e-3: 7, - 2e-3: 8, - 1e-3: 9, - 500e-6: 10, - 200e-6: 11, - 100e-6: 12, - 50e-6: 13, - 20e-6: 14, - 10e-6: 15, - 5e-6: 16, - 2e-6: 17, - 1e-6: 18, - 500e-9: 19, - 200e-9: 20, - 100e-9: 21, - 50e-9: 22, - 20e-9: 23, - 10e-9: 24, - 5e-9: 25, - 2e-9: 26, - 1e-9: 27, - } - _N_TO_VOLT: ClassVar[dict[int, int | float]] = {v: k for k, v in _VOLT_TO_N.items()} - - _CURR_TO_N: ClassVar[dict[float, int]] = { - 1e-6: 0, - 500e-9: 1, - 200e-9: 2, - 100e-9: 3, - 50e-9: 4, - 20e-9: 5, - 10e-9: 6, - 5e-9: 7, - 2e-9: 8, - 1e-9: 9, - 500e-12: 10, - 200e-12: 11, - 100e-12: 12, - 50e-12: 13, - 20e-12: 14, - 10e-12: 15, - 5e-12: 16, - 2e-12: 17, - 1e-12: 18, - 500e-15: 19, - 200e-15: 20, - 100e-15: 21, - 50e-15: 22, - 20e-15: 23, - 10e-15: 24, - 5e-15: 25, - 2e-15: 26, - 1e-15: 27, - } - _N_TO_CURR: ClassVar[dict[int, float]] = {v: k for k, v in _CURR_TO_N.items()} - - _VOLT_ENUM = Enum(*_VOLT_TO_N.keys()) - _CURR_ENUM = Enum(*_CURR_TO_N.keys()) - - _INPUT_SIGNAL_TO_N: ClassVar[dict[str, int]] = { - "voltage": 0, - "current": 1, - } - _N_TO_INPUT_SIGNAL: ClassVar[dict[int, str]] = { - v: k for k, v in _INPUT_SIGNAL_TO_N.items() - } - - PARAMETER_NAMES: ClassVar[dict[str, str]] = { - "X": "0", # X output, 'X' - "Y": "1", # Y output, 'Y' - "R": "2", # R output, 'R' - "P": "3", # theta output, 'THeta' - "aux_in1": "4", # Aux In 1, 'IN1' - "aux_in2": "5", # Aux In 2, 'IN2' - "aux_in3": "6", # Aux In 3, 'IN3' - "aux_in4": "7", # Aux In 4, 'IN4' - "Xnoise": "8", # X noise, 'XNOise' - "Ynoise": "9", # Y noise, 'YNOise' - "aux_out1": "10", # Aux Out 1, 'OUT1' - "aux_out2": "11", # Aux Out 2, 'OUT2' - "phase": "12", # Reference Phase, 'PHAse' - "amplitude": "13", # Sine Out Amplitude, 'SAMp' - "sine_outdc": "14", # DC Level, 'LEVel' - "frequency": "15", # Int. Ref. Frequency, 'FInt' - "frequency_ext": "16", # Ext. Ref. Frequency, 'FExt' - } - - _N_DATA_CHANNELS = 4 - - default_terminator = "\n" - - def __init__( - self, - name: str, - address: str, - max_frequency: float, - reset: bool = False, - **kwargs: Unpack[VisaInstrumentKWArgs], - ): - super().__init__(name, address, **kwargs) - self._max_frequency = max_frequency - # Reference commands - self.frequency: Parameter[float, Self] = self.add_parameter( - name="frequency", - label="Frequency", - unit="Hz", - get_cmd="FREQ?", - set_cmd="FREQ {}", - get_parser=float, - vals=Numbers(min_value=1e-3, max_value=self._max_frequency), - ) - """Parameter frequency""" - self.sine_outdc: Parameter[float, Self] = self.add_parameter( - name="sine_outdc", - label="Sine out dc level", - unit="V", - get_cmd="SOFF?", - set_cmd="SOFF {}", - get_parser=float, - vals=Numbers(min_value=-5, max_value=5), - ) - """Parameter sine_outdc""" - self.amplitude: Parameter[float, Self] = self.add_parameter( - name="amplitude", - label="Amplitude", - unit="V", - get_cmd="SLVL?", - set_cmd="SLVL {}", - get_parser=float, - vals=Numbers(min_value=0, max_value=2), - ) - """Parameter amplitude""" - self.harmonic: Parameter[int, Self] = self.add_parameter( - name="harmonic", - label="Harmonic", - get_cmd="HARM?", - get_parser=int, - set_cmd="HARM {:d}", - vals=Ints(min_value=1, max_value=99), - ) - """Parameter harmonic""" - self.phase: Parameter[float, Self] = self.add_parameter( - name="phase", - label="Phase", - unit="deg", - get_cmd="PHAS?", - set_cmd="PHAS {}", - get_parser=float, - vals=Numbers(min_value=-3.6e5, max_value=3.6e5), - ) - """Parameter phase""" - # Signal commands - self.sensitivity: Parameter[float, Self] = self.add_parameter( - name="sensitivity", - label="Sensitivity", - get_cmd="SCAL?", - set_cmd="SCAL {:d}", - get_parser=self._get_sensitivity, - set_parser=self._set_sensitivity, - ) - """Parameter sensitivity""" - self.filter_slope: Parameter[int, Self] = self.add_parameter( - name="filter_slope", - label="Filter slope", - unit="dB/oct", - get_cmd="OFSL?", - set_cmd="OFSL {}", - val_mapping={6: 0, 12: 1, 18: 2, 24: 3}, - ) - """Parameter filter_slope""" - self.sync_filter: Parameter[str, Self] = self.add_parameter( - name="sync_filter", - label="Sync filter", - get_cmd="SYNC?", - set_cmd="SYNC {}", - val_mapping={"OFF": 0, "ON": 1}, - ) - """Parameter sync_filter""" - self.noise_bandwidth: Parameter[float, Self] = self.add_parameter( - name="noise_bandwidth", - label="Noise bandwidth", - unit="Hz", - get_cmd="ENBW?", - get_parser=float, - ) - """Parameter noise_bandwidth""" - self.signal_strength: Parameter[int, Self] = self.add_parameter( - name="signal_strength", - label="Signal strength indicator", - get_cmd="ILVL?", - get_parser=int, - ) - """Parameter signal_strength""" - self.signal_input: Parameter[str, Self] = self.add_parameter( - name="signal_input", - label="Signal input", - get_cmd="IVMD?", - get_parser=self._get_input_config, - set_cmd="IVMD {}", - set_parser=self._set_input_config, - vals=Enum(*self._INPUT_SIGNAL_TO_N.keys()), - ) - """Parameter signal_input""" - self.input_range: Parameter = self.add_parameter( - name="input_range", - label="Input range", - unit="V", - get_cmd="IRNG?", - set_cmd="IRNG {}", - val_mapping={1: 0, 300e-3: 1, 100e-3: 2, 30e-3: 3, 10e-3: 4}, - ) - """Parameter input_range""" - self.input_config: Parameter[str, Self] = self.add_parameter( - name="input_config", - label="Input configuration", - get_cmd="ISRC?", - set_cmd="ISRC {}", - val_mapping={"a": 0, "a-b": 1}, - ) - """Parameter input_config""" - self.input_shield: Parameter[str, Self] = self.add_parameter( - name="input_shield", - label="Input shield", - get_cmd="IGND?", - set_cmd="IGND {}", - val_mapping={"float": 0, "ground": 1}, - ) - """Parameter input_shield""" - self.input_gain: Parameter[float, Self] = self.add_parameter( - name="input_gain", - label="Input gain", - unit="ohm", - get_cmd="ICUR?", - set_cmd="ICUR {}", - val_mapping={1e6: 0, 100e6: 1}, - ) - """Parameter input_gain""" - self.adv_filter: Parameter[str, Self] = self.add_parameter( - name="adv_filter", - label="Advanced filter", - get_cmd="ADVFILT?", - set_cmd="ADVFILT {}", - val_mapping={"OFF": 0, "ON": 1}, - ) - """Parameter adv_filter""" - self.input_coupling: Parameter[str, Self] = self.add_parameter( - name="input_coupling", - label="Input coupling", - get_cmd="ICPL?", - set_cmd="ICPL {}", - val_mapping={"ac": 0, "dc": 1}, - ) - """Parameter input_coupling""" - self.time_constant: Parameter = self.add_parameter( - name="time_constant", - label="Time constant", - unit="s", - get_cmd="OFLT?", - set_cmd="OFLT {}", - val_mapping={ - 1e-6: 0, - 3e-6: 1, - 10e-6: 2, - 30e-6: 3, - 100e-6: 4, - 300e-6: 5, - 1e-3: 6, - 3e-3: 7, - 10e-3: 8, - 30e-3: 9, - 100e-3: 10, - 300e-3: 11, - 1: 12, - 3: 13, - 10: 14, - 30: 15, - 100: 16, - 300: 17, - 1e3: 18, - 3e3: 19, - 10e3: 20, - 30e3: 21, - }, - ) - """Parameter time_constant""" - - self.external_reference_trigger: Parameter[str, Self] = self.add_parameter( - name="external_reference_trigger", - label="External reference trigger mode", - get_cmd="RTRG?", - set_cmd="RTRG {}", - val_mapping={ - "SIN": 0, - "POS": 1, - "POSTTL": 1, - "NEG": 2, - "NEGTTL": 2, - }, - docstring="The triggering mode for synchronization of the " - "internal reference signal with the externally provided " - "one", - ) - """The triggering mode for synchronization of the internal reference signal with the externally provided one""" - - self.reference_source: Parameter[str, Self] = self.add_parameter( - name="reference_source", - label="Reference source", - get_cmd="RSRC?", - set_cmd="RSRC {}", - val_mapping={"INT": 0, "EXT": 1, "DUAL": 2, "CHOP": 3}, - docstring="The source of the reference signal", - ) - """The source of the reference signal""" - - self.external_reference_trigger_input_resistance: Parameter = ( - self.add_parameter( - name="external_reference_trigger_input_resistance", - label="External reference trigger input resistance", - get_cmd="REFZ?", - set_cmd="REFZ {}", - val_mapping={ - "50": 0, - "50OHMS": 0, - 0: 0, - "1M": 1, - "1MEG": 1, - 1: 1, - }, - docstring="Input resistance of the input for the external " - "reference signal", - ) - ) - """Input resistance of the input for the external reference signal""" - - # Auto functions - self.add_function("auto_range", call_cmd="ARNG") - self.add_function("auto_scale", call_cmd="ASCL") - self.add_function("auto_phase", call_cmd="APHS") - - # Data transfer - # first 4 parameters from a list of 16 below. - self.X: Parameter[float, Self] = self.add_parameter( - "X", - label="In-phase Magnitude", - get_cmd="OUTP? 0", - get_parser=float, - unit="V", - ) - """Parameter X""" - self.Y: Parameter[float, Self] = self.add_parameter( - "Y", - label="Out-phase Magnitude", - get_cmd="OUTP? 1", - get_parser=float, - unit="V", - ) - """Parameter Y""" - self.R: Parameter[float, Self] = self.add_parameter( - "R", label="Magnitude", get_cmd="OUTP? 2", get_parser=float, unit="V" - ) - """Parameter R""" - self.P: Parameter[float, Self] = self.add_parameter( - "P", label="Phase", get_cmd="OUTP? 3", get_parser=float, unit="deg" - ) - """Parameter P""" - - self.complex_voltage: Parameter = self.add_parameter( - "complex_voltage", - label="Voltage", - get_cmd=self._get_complex_voltage, - unit="V", - vals=ComplexNumbers(), - ) - """Parameter complex_voltage""" - - # CH1/CH2 Output Commands - self.X_offset: Parameter[float, Self] = self.add_parameter( - "X_offset", - label="X offset ", - unit="%", - get_cmd="COFP? 0", - set_cmd="COFP 0, {}", - get_parser=float, - vals=Numbers(min_value=-999.99, max_value=999.99), - ) - """Parameter X_offset""" - self.Y_offset: Parameter[float, Self] = self.add_parameter( - "Y_offset", - label="Y offset", - unit="%", - get_cmd="COFP? 1", - set_cmd="COFP 1, {}", - get_parser=float, - vals=Numbers(min_value=-999.99, max_value=999.99), - ) - """Parameter Y_offset""" - self.R_offset: Parameter[float, Self] = self.add_parameter( - "R_offset", - label="R offset", - unit="%", - get_cmd="COFP? 2", - set_cmd="COFP 2, {}", - get_parser=float, - vals=Numbers(min_value=-999.99, max_value=999.99), - ) - """Parameter R_offset""" - self.X_expand: Parameter[str, Self] = self.add_parameter( - "X_expand", - label="X expand multiplier", - get_cmd="CEXP? 0", - set_cmd="CEXP 0, {}", - val_mapping={"OFF": "0", "X10": "1", "X100": "2"}, - ) - """Parameter X_expand""" - self.Y_expand: Parameter[str, Self] = self.add_parameter( - "Y_expand", - label="Y expand multiplier", - get_cmd="CEXP? 1", - set_cmd="CEXP 1, {}", - val_mapping={"OFF": 0, "X10": 1, "X100": 2}, - ) - """Parameter Y_expand""" - self.R_expand: Parameter[str, Self] = self.add_parameter( - "R_expand", - label="R expand multiplier", - get_cmd="CEXP? 2", - set_cmd="CEXP 2, {}", - val_mapping={"OFF": 0, "X10": 1, "X100": 2}, - ) - """Parameter R_expand""" - - # Aux input/output - for i in [0, 1, 2, 3]: - self.add_parameter( - f"aux_in{i}", - label=f"Aux input {i}", - get_cmd=f"OAUX? {i}", - get_parser=float, - unit="V", - ) - self.add_parameter( - f"aux_out{i}", - label=f"Aux output {i}", - get_cmd=f"AUXV? {i}", - get_parser=float, - set_cmd=f"AUXV {i}, {{}}", - unit="V", - ) - - # Data channels: - # 'DAT1' (green), 'DAT2' (blue), 'DAT3' (yellow), 'DAT4' (orange) - data_channels = ChannelList( - self, "data_channels", SR86xDataChannel, snapshotable=False - ) - for num, color in zip( - range(self._N_DATA_CHANNELS), ("green", "blue", "yellow", "orange") - ): - cmd_id = f"{num}" - cmd_id_name = f"DAT{num + 1}" - ch_name = f"data_channel_{num + 1}" - - data_channel = SR86xDataChannel(self, ch_name, cmd_id, cmd_id_name, color) - - data_channels.append(data_channel) - self.add_submodule(ch_name, data_channel) - - self.data_channels: ChannelTuple[SR86xDataChannel] = self.add_submodule( - "data_channels", data_channels.to_channel_tuple() - ) - """Interface for the SR86x data channels""" - - # Interface - self.add_function("reset", call_cmd="*RST") - - self.add_function("disable_front_panel", call_cmd="OVRM 0") - self.add_function("enable_front_panel", call_cmd="OVRM 1") - - buffer = SR86xBuffer(self, f"{self.name}_buffer") - self.buffer: SR86xBuffer = self.add_submodule("buffer", buffer) - """Interface for the SR86x buffer""" - - self.input_config() - self.connect_message() - - def _set_units(self, unit: str) -> None: - for param in [self.X, self.Y, self.R, self.sensitivity]: - param.unit = unit - - def _get_complex_voltage(self) -> complex: - x, y = self.get_values("X", "Y") - return x + 1.0j * y - - def _get_input_config(self, s: int) -> str: - mode = self._N_TO_INPUT_SIGNAL[int(s)] - - if mode == "voltage": - self.sensitivity.vals = self._VOLT_ENUM - self._set_units("V") - else: - self.sensitivity.vals = self._CURR_ENUM - self._set_units("A") - - return mode - - def _set_input_config(self, s: str) -> int: - if s == "voltage": - self.sensitivity.vals = self._VOLT_ENUM - self._set_units("V") - else: - self.sensitivity.vals = self._CURR_ENUM - self._set_units("A") - - return self._INPUT_SIGNAL_TO_N[s] - - def _get_sensitivity(self, s: int) -> float: - if self.signal_input() == "voltage": - return self._N_TO_VOLT[int(s)] - else: - return self._N_TO_CURR[int(s)] - - def _set_sensitivity(self, s: float) -> int: - if self.signal_input() == "voltage": - return self._VOLT_TO_N[s] - else: - return self._CURR_TO_N[s] - - def get_values(self, *parameter_names: str) -> tuple[float, ...]: - """ - Get values of 2 or 3 parameters that are measured by the lock-in - amplifier. These values are guaranteed to come from the same - measurement cycle as opposed to getting values of parameters one by - one (for example, by calling `sr.X()`, and then `sr.Y()`. - - Args: - *parameter_names: 2 or 3 names of parameters for which the values - are requested; valid names can be found in `PARAMETER_NAMES` - attribute of the driver class - - Returns: - a tuple of 2 or 3 floating point values - - """ - if not 2 <= len(parameter_names) <= 3: - raise KeyError( - "It is only possible to request values of 2 or 3 parameters at a time." - ) - - for name in parameter_names: - if name not in self.PARAMETER_NAMES: - raise KeyError( - f"{name} is not a valid parameter name. Refer " - f"to `PARAMETER_NAMES` for a list of valid " - f"parameter names" - ) - - p_ids = [self.PARAMETER_NAMES[name] for name in parameter_names] - output = self.ask(f"SNAP? {','.join(p_ids)}") - return tuple(float(val) for val in output.split(",")) - - def get_data_channels_values(self) -> tuple[float, ...]: - """ - Queries the current values of the data channels - - Returns: - tuple of 4 values of the data channels - - """ - output = self.ask("SNAPD?") - return tuple(float(val) for val in output.split(",")) - - def get_data_channels_parameters( - self, query_instrument: bool = True - ) -> tuple[str, ...]: - """ - Convenience method to query a list of parameters which the data - channels are currently assigned to. - - Args: - query_instrument: If set to False, the internally cashed names of - the parameters will be returned; if True, then the names will - be queried through the instrument - - Returns: - a tuple of 4 strings of parameter names - - """ - if query_instrument: - method_name = "get" - else: - method_name = "get_latest" - - return tuple( - getattr(self.data_channels[i].assigned_parameter, method_name)() - for i in range(self._N_DATA_CHANNELS) - ) - - def get_data_channels_dict(self, requery_names: bool = False) -> dict[str, float]: - """ - Returns a dictionary where the keys are parameter names currently - assigned to the data channels, and values are the values of those - parameters. - - Args: - requery_names: if False, the currently assigned parameter names - will not be queries from the instrument in order to save time - on communication, in this case the cached assigned parameter - names will be used for the keys of the dicitonary; if True, - the assigned parameter names will be queried from the - instrument +from qcodes.utils.deprecate import _warn_deprecated_module - Returns: - a dictionary where keys are names of parameters assigned to the - data channels, and values are the values of those parameters +_warn_deprecated_module(__name__, "qcodes.instrument_drivers.stanford_research") - """ - parameter_names = self.get_data_channels_parameters(requery_names) - parameter_values = self.get_data_channels_values() - return dict(zip(parameter_names, parameter_values)) +__all__ = ["SR86x", "SR86xBuffer", "SR86xBufferReadout", "SR86xDataChannel"] diff --git a/src/qcodes/instrument_drivers/stanford_research/__init__.py b/src/qcodes/instrument_drivers/stanford_research/__init__.py index 475798cd1aa2..ed9affc74bbc 100644 --- a/src/qcodes/instrument_drivers/stanford_research/__init__.py +++ b/src/qcodes/instrument_drivers/stanford_research/__init__.py @@ -1,10 +1,17 @@ -from .SG384 import SG384 -from .SR86x import SR86x, SR86xBuffer, SR86xDataChannel -from .SR560 import SR560 -from .SR830 import SR830 -from .SR860 import SR860 -from .SR865 import SR865 -from .SR865A import SR865A +from qcodes.utils.deprecate import _keep_attributes_shadowing_submodules + +from ._sg384 import SG384 +from ._sr86x import SR86x, SR86xBuffer, SR86xDataChannel +from ._sr560 import SR560, SR560VoltageParameter +from ._sr830 import SR830 +from ._sr860 import SR860 +from ._sr865 import SR865 +from ._sr865a import SR865A + +# Keep the public objects when the deprecated module aliases that share +# their names are imported. +_keep_attributes_shadowing_submodules(__name__) + __all__ = [ "SG384", @@ -16,4 +23,5 @@ "SR86x", "SR86xBuffer", "SR86xDataChannel", + "SR560VoltageParameter", ] diff --git a/src/qcodes/instrument_drivers/stanford_research/_sg384.py b/src/qcodes/instrument_drivers/stanford_research/_sg384.py new file mode 100644 index 000000000000..d65181fbe4e7 --- /dev/null +++ b/src/qcodes/instrument_drivers/stanford_research/_sg384.py @@ -0,0 +1,226 @@ +from typing import TYPE_CHECKING, Self + +from qcodes import validators as vals +from qcodes.instrument import VisaInstrument, VisaInstrumentKWArgs + +if TYPE_CHECKING: + from typing import Unpack + + from qcodes.parameters import Parameter + + +class SG384(VisaInstrument): + """ + QCoDeS driver for the Stanford Research Systems SG384 RF Signal Generator. + + Status: beta version + Includes the essential commands from the manual + """ + + default_terminator = "\n" + + def __init__( + self, + name: str, + address: str, + reset: bool = False, + **kwargs: "Unpack[VisaInstrumentKWArgs]", + ): + super().__init__(name, address, **kwargs) + # signal synthesis commands + self.frequency: Parameter[float, Self] = self.add_parameter( + name="frequency", + label="Frequency", + unit="Hz", + get_cmd="FREQ?", + set_cmd="FREQ {:.3f}", + get_parser=float, + vals=vals.Numbers(min_value=9.5e5, max_value=4.05e9), + ) + """Parameter frequency""" + self.phase: Parameter[float, Self] = self.add_parameter( + name="phase", + label="Carrier phase", + unit="deg", + get_cmd="PHAS?", + set_cmd="PHAS {:.1f}", + get_parser=float, + vals=vals.Numbers(min_value=-360, max_value=360), + ) + """Parameter phase""" + self.amplitude_LF: Parameter[float, Self] = self.add_parameter( + name="amplitude_LF", + label="Power of BNC output", + unit="dBm", + get_cmd="AMPL?", + set_cmd="AMPL {:.2f}", + get_parser=float, + vals=vals.Numbers(min_value=-47, max_value=13), + ) + """Parameter amplitude_LF""" + self.amplitude_RF: Parameter[float, Self] = self.add_parameter( + name="amplitude_RF", + label="Power of type-N RF output", + unit="dBm", + get_cmd="AMPR?", + set_cmd="AMPR {:.2f}", + get_parser=float, + vals=vals.Numbers(min_value=-110, max_value=16.5), + ) + """Parameter amplitude_RF""" + self.amplitude_HF: Parameter[float, Self] = self.add_parameter( + name="amplitude_HF", + label="Power of RF doubler output", + unit="dBm", + get_cmd="AMPH?", + set_cmd="AMPH {:.2f}", + get_parser=float, + vals=vals.Numbers(min_value=-10, max_value=13), + ) + """Parameter amplitude_HF""" + self.amplitude_clock: Parameter[float, Self] = self.add_parameter( + name="amplitude_clock", + label="Rear clock output", + unit="Vpp", + get_cmd="AMPC?", + set_cmd="AMPC {:.2f}", + get_parser=float, + vals=vals.Numbers(min_value=0.4, max_value=1.00), + ) + """Parameter amplitude_clock""" + self.noise_mode: Parameter[str, Self] = self.add_parameter( + name="noise_mode", + label="RF PLL loop filter mode", + get_cmd="NOIS?", + set_cmd="NOIS {}", + val_mapping={"Mode 1": 0, "Mode 2": 1}, + ) + """Parameter noise_mode""" + self.enable_RF: Parameter[str, Self] = self.add_parameter( + name="enable_RF", + label="Type-N RF output", + get_cmd="ENBR?", + set_cmd="ENBR {}", + val_mapping={"OFF": 0, "ON": 1}, + ) + """Parameter enable_RF""" + self.enable_LF: Parameter[str, Self] = self.add_parameter( + name="enable_LF", + label="BNC output", + get_cmd="ENBL?", + set_cmd="ENBL {}", + val_mapping={"OFF": 0, "ON": 1}, + ) + """Parameter enable_LF""" + self.enable_HF: Parameter[str, Self] = self.add_parameter( + name="enable_HF", + label="RF doubler output", + get_cmd="ENBH?", + set_cmd="ENBH {}", + val_mapping={"OFF": 0, "ON": 1}, + ) + """Parameter enable_HF""" + self.enable_clock: Parameter[str, Self] = self.add_parameter( + name="enable_clock", + label="Rear clock output", + get_cmd="ENBC?", + set_cmd="ENBC {}", + val_mapping={"OFF": 0, "ON": 1}, + ) + """Parameter enable_clock""" + self.offset_clock: Parameter[float, Self] = self.add_parameter( + name="offset_clock", + label="Rear clock offset voltage", + unit="V", + get_cmd="OFSC?", + set_cmd="OFSC {}", + get_parser=float, + vals=vals.Numbers(min_value=-2, max_value=2), + ) + """Parameter offset_clock""" + self.offset_rearDC: Parameter[float, Self] = self.add_parameter( + name="offset_rearDC", + label="Rear DC offset voltage", + unit="V", + get_cmd="OFSD?", + set_cmd="OFSD {}", + get_parser=float, + vals=vals.Numbers(min_value=-10, max_value=10), + ) + """Parameter offset_rearDC""" + self.offset_bnc: Parameter[float, Self] = self.add_parameter( + name="offset_bnc", + label="Low frequency BNC output", + unit="V", + get_cmd="OFSL?", + set_cmd="OFSL {}", + get_parser=float, + vals=vals.Numbers(min_value=-1.5, max_value=1.5), + ) + """Parameter offset_bnc""" + # Modulation commands + self.modulation_coupling: Parameter[str, Self] = self.add_parameter( + name="modulation_coupling", + label="External modulation input coupling", + get_cmd="COUP?", + set_cmd="COUP {}", + val_mapping={"AC": 0, "DC": 1}, + ) + """Parameter modulation_coupling""" + self.FM_deviation: Parameter[float, Self] = self.add_parameter( + name="FM_deviation", + label="Frequency modulation deviation", + unit="Hz", + get_cmd="FDEV?", + set_cmd="FDEV {:.1f}", + get_parser=float, + vals=vals.Numbers(min_value=0.1, max_value=32e6), + ) + """Parameter FM_deviation""" + self.modulation_function: Parameter[str, Self] = self.add_parameter( + name="modulation_function", + label="Modulation function for AM/FM/PhiM", + get_cmd="MFNC?", + set_cmd="MFNC {}", + val_mapping={ + "Sine": 0, + "Ramp": 1, + "Triangle": 2, + "Square": 3, + "Noise": 4, + "External": 5, + }, + ) + """Parameter modulation_function""" + self.enable_modulation: Parameter[str, Self] = self.add_parameter( + name="enable_modulation", + get_cmd="MODL?", + set_cmd="MODL {}", + val_mapping={"OFF": 0, "ON": 1}, + ) + """Parameter enable_modulation""" + self.modulation_rate: Parameter[float, Self] = self.add_parameter( + name="modulation_rate", + get_cmd="RATE?", + set_cmd="RATE {:.6f}", + get_parser=float, + vals=vals.Numbers(min_value=1e-6, max_value=50e3), + ) + """Parameter modulation_rate""" + self.modulation_type: Parameter[str, Self] = self.add_parameter( + name="modulation_type", + label="Current modulation type", + get_cmd="TYPE?", + set_cmd="TYPE {}", + val_mapping={ + "AM": 0, + "FM": 1, + "Phi": 2, + "Sweep": 3, + "Pulse": 4, + "Blank": 5, + "IQ": 6, + }, + ) + """Parameter modulation_type""" + self.connect_message() diff --git a/src/qcodes/instrument_drivers/stanford_research/_sr560.py b/src/qcodes/instrument_drivers/stanford_research/_sr560.py new file mode 100644 index 000000000000..22ec86889899 --- /dev/null +++ b/src/qcodes/instrument_drivers/stanford_research/_sr560.py @@ -0,0 +1,183 @@ +from typing import TYPE_CHECKING, Self + +from qcodes.instrument import Instrument, InstrumentBaseKWArgs +from qcodes.parameters import MultiParameter, Parameter, ParamRawDataType +from qcodes.validators import Bool, Enum + +if TYPE_CHECKING: + from typing import Unpack + + +class SR560VoltageParameter(MultiParameter): + """ + Amplified voltage measurement via an SR560 preamp and a measured voltage. + + To be used when you feed a voltage into an SR560, send the SR560's + output voltage to a lockin or other voltage amplifier, and you have + the voltage reading from that amplifier as a qcodes parameter. + + ``SR560VoltageParameter.get()`` returns ``(voltage_raw, voltage)`` + + Args: + measured_param: a gettable parameter returning the + voltage read from the SR560 output. + v_amp_ins: an SR560 instance where you manually + maintain the present settings of the real SR560 amp. + + Note: it should be possible to use other voltage preamps, if they + define parameters ``gain`` (V_out / V_in) and ``invert`` + (bool, output is inverted) + + name: the name of the current output. Default 'volt'. + Also used as the name of the whole parameter. + + """ + + def __init__( + self, + measured_param: Parameter, + v_amp_ins: "SR560", + name: str = "volt", + snapshot_value: bool = True, + ): + p_name = measured_param.name + + super().__init__( + name=name, + names=(p_name + "_raw", name), + shapes=((), ()), + instrument=v_amp_ins, + ) + + self._measured_param = measured_param + + p_label = getattr(measured_param, "label", "") + p_unit = getattr(measured_param, "unit", "") + + self.labels = (p_label, "Voltage") + self.units = (p_unit, "V") + + def get_raw(self) -> tuple[ParamRawDataType, ParamRawDataType]: + volt = self._measured_param.get() + assert isinstance(self.instrument, SR560) + volt_amp = volt / self.instrument.gain.get() + + if self.instrument.invert.get(): + volt_amp *= -1 + + value = (volt, volt_amp) + return value + + +class SR560(Instrument): + """ + QCoDeS driver for the Stanford Research Systems SR560 Voltage-preamplifier. + + This is a virtual driver only and will not talk to your instrument. + + Note: + - The ``cutoff_lo`` and ``cutoff_hi`` parameters will interact with + each other on the instrument (hi cannot be <= lo) but this is not + managed here, you must ensure yourself that both are correct whenever + you change one of them. + - ``gain`` has a vernier setting, which does not yield a well-defined + output. We restrict this driver to only the predefined gain values. + + """ + + def __init__(self, name: str, **kwargs: "Unpack[InstrumentBaseKWArgs]"): + super().__init__(name, **kwargs) + + cutoffs = [ + "DC", + 0.03, + 0.1, + 0.3, + 1, + 3, + 10, + 30, + 100, + 300, + 1000, + 3000, + 10000, + 30000, + 100000, + 300000, + 1000000, + ] + + gains = [ + 1, + 2, + 5, + 10, + 20, + 50, + 100, + 200, + 500, + 1000, + 2000, + 5000, + 10000, + 20000, + 50000, + ] + + self.cutoff_lo: Parameter = self.add_parameter( + "cutoff_lo", + get_cmd=None, + set_cmd=None, + initial_value="DC", + label="High pass", + unit="Hz", + vals=Enum(*cutoffs), + ) + """Parameter cutoff_lo""" + + self.cutoff_hi: Parameter = self.add_parameter( + "cutoff_hi", + get_cmd=None, + set_cmd=None, + initial_value=1e6, + label="Low pass", + unit="Hz", + vals=Enum(*cutoffs), + ) + """Parameter cutoff_hi""" + + self.invert: Parameter[bool, Self] = self.add_parameter( + "invert", + get_cmd=None, + set_cmd=None, + initial_value=True, + label="Inverted output", + vals=Bool(), + ) + """Parameter invert""" + + self.gain: Parameter = self.add_parameter( + "gain", + get_cmd=None, + set_cmd=None, + initial_value=10, + label="Gain", + unit=None, + vals=Enum(*gains), + ) + """Parameter gain""" + + def get_idn(self) -> dict[str, str | None]: + vendor = "Stanford Research Systems" + model = "SR560" + serial = None + firmware = None + + return { + "vendor": vendor, + "model": model, + "serial": serial, + "firmware": firmware, + } diff --git a/src/qcodes/instrument_drivers/stanford_research/_sr830.py b/src/qcodes/instrument_drivers/stanford_research/_sr830.py new file mode 100644 index 000000000000..ed53014f0c3c --- /dev/null +++ b/src/qcodes/instrument_drivers/stanford_research/_sr830.py @@ -0,0 +1,1034 @@ +from __future__ import annotations + +import time +from functools import partial +from typing import TYPE_CHECKING, Any, ClassVar, Self + +import numpy as np + +from qcodes.instrument import VisaInstrument, VisaInstrumentKWArgs +from qcodes.parameters import ( + ArrayParameter, + Parameter, + ParameterWithSetpoints, + ParamRawDataType, +) +from qcodes.validators import Arrays, ComplexNumbers, Enum, Ints, Numbers, Strings + +if TYPE_CHECKING: + from collections.abc import Iterable + from typing import Unpack + + +class SR830ChannelTrace(ParameterWithSetpoints): + """ + Parameter class for the two channel buffers + """ + + def __init__(self, name: str, channel: int, **kwargs: Any) -> None: + """ + Args: + name: The name of the parameter + channel: The relevant channel (1 or 2). The name should + match this. + **kwargs: kwargs are forwarded to base class. + + """ + super().__init__(name, **kwargs) + + self._valid_channels = (1, 2) + + if channel not in self._valid_channels: + raise ValueError( + "Invalid channel specifier. SR830 only has channels 1 and 2." + ) + + if not isinstance(self.root_instrument, SR830): + raise ValueError( # noqa: TRY004 + "Invalid parent instrument. SR830ChannelTrace can only live on an SR830." + ) + + self.channel = channel + self.update_unit() + + def update_unit(self) -> None: + assert isinstance(self.root_instrument, SR830) + params = self.root_instrument.parameters + if params[f"ch{self.channel}_ratio"].get() != "none": + self.unit = "%" + else: + disp = params[f"ch{self.channel}_display"].get() + if disp == "Phase": + self.unit = "deg" + else: + self.unit = "V" + self.label = disp + + def get_raw(self) -> ParamRawDataType: + """ + Get command. Returns numpy array + """ + assert isinstance(self.root_instrument, SR830) + N = self.root_instrument.buffer_npts() + if N == 0: + raise ValueError( + "No points stored in SR830 data buffer. Can not poll anything." + ) + + # poll raw binary data + self.root_instrument.write(f"TRCL ? {self.channel}, 0, {N}") + rawdata = self.root_instrument.visa_handle.read_raw() + + # parse it + realdata = np.frombuffer(rawdata, dtype=" None: + """ + Args: + name: The name of the parameter + instrument: The parent instrument + channel: The relevant channel (1 or 2). The name should + should match this. + + """ + self._valid_channels = (1, 2) + + if channel not in self._valid_channels: + raise ValueError( + "Invalid channel specifier. SR830 only has channels 1 and 2." + ) + + if not isinstance(instrument, SR830): + raise ValueError( # noqa: TRY004 + "Invalid parent instrument. SR830ChannelBuffer can only live on an SR830." + ) + + super().__init__( + name, + shape=(1,), # dummy initial shape + unit="V", # dummy initial unit + setpoint_names=("Time",), + setpoint_labels=("Time",), + setpoint_units=("s",), + docstring="Holds an acquired (part of the) data buffer of one channel.", + instrument=instrument, + ) + + self.channel = channel + + def prepare_buffer_readout(self) -> None: + """ + Function to generate the setpoints for the channel buffer and + get the right units + """ + assert isinstance(self.instrument, SR830) + N = self.instrument.buffer_npts() # problem if this is zero? + # TODO (WilliamHPNielsen): what if SR was changed during acquisition? + SR = self.instrument.buffer_SR() + if SR == "Trigger": + self.setpoint_units = ("",) + self.setpoint_names = ("trig_events",) + self.setpoint_labels = ("Trigger event number",) + self.setpoints = (tuple(np.arange(0, N)),) + else: + dt = 1 / SR + self.setpoint_units = ("s",) + self.setpoint_names = ("Time",) + self.setpoint_labels = ("Time",) + self.setpoints = (tuple(np.linspace(0, N * dt, N)),) + + self.shape = (N,) + + params = self.instrument.parameters + # YES, it should be: comparing to the string 'none' and not + # the None literal + if params[f"ch{self.channel}_ratio"].get() != "none": + self.unit = "%" + else: + disp = params[f"ch{self.channel}_display"].get() + if disp == "Phase": + self.unit = "deg" + else: + self.unit = "V" + + if self.channel == 1: + self.instrument._buffer1_ready = True + else: + self.instrument._buffer2_ready = True + + def get_raw(self) -> ParamRawDataType: + """ + Get command. Returns numpy array + """ + assert isinstance(self.instrument, SR830) + if self.channel == 1: + ready = self.instrument._buffer1_ready + else: + ready = self.instrument._buffer2_ready + + if not ready: + raise RuntimeError("Buffer not ready. Please run prepare_buffer_readout") + N = self.instrument.buffer_npts() + if N == 0: + raise ValueError( + "No points stored in SR830 data buffer. Can not poll anything." + ) + + # poll raw binary data + self.instrument.write(f"TRCL ? {self.channel}, 0, {N}") + rawdata = self.instrument.visa_handle.read_raw() + + # parse it + realdata = np.frombuffer(rawdata, dtype=" tuple[float, int]: + parts = s.split(",") + + return float(parts[0]), int(parts[1]) + + # TODO: Parameters that can be set with multiple arguments + # For the OEXP command for example two arguments are needed + self.X_offset: Parameter[tuple[float, int], Self] = self.add_parameter( + "X_offset", get_cmd="OEXP? 1", get_parser=parse_offset_get + ) + """Parameter X_offset""" + + self.Y_offset: Parameter[tuple[float, int], Self] = self.add_parameter( + "Y_offset", get_cmd="OEXP? 2", get_parser=parse_offset_get + ) + """Parameter Y_offset""" + + self.R_offset: Parameter[tuple[float, int], Self] = self.add_parameter( + "R_offset", get_cmd="OEXP? 3", get_parser=parse_offset_get + ) + """Parameter R_offset""" + + # Aux input/output + for i in [1, 2, 3, 4]: + self.add_parameter( + f"aux_in{i}", + label=f"Aux input {i}", + get_cmd=f"OAUX? {i}", + get_parser=float, + unit="V", + ) + + self.add_parameter( + f"aux_out{i}", + label=f"Aux output {i}", + get_cmd=f"AUXV? {i}", + get_parser=float, + set_cmd=f"AUXV {i}, {{}}", + unit="V", + ) + + # Setup + self.output_interface: Parameter[str, Self] = self.add_parameter( + "output_interface", + label="Output interface", + get_cmd="OUTX?", + set_cmd="OUTX {}", + val_mapping={ + "RS232": "0\n", + "GPIB": "1\n", + }, + ) + """Parameter output_interface""" + + # Data transfer + self.X: Parameter[float, Self] = self.add_parameter( + "X", get_cmd="OUTP? 1", get_parser=float, unit="V" + ) + """Parameter X""" + + self.Y: Parameter[float, Self] = self.add_parameter( + "Y", get_cmd="OUTP? 2", get_parser=float, unit="V" + ) + """Parameter Y""" + + self.R: Parameter[float, Self] = self.add_parameter( + "R", get_cmd="OUTP? 3", get_parser=float, unit="V" + ) + """Parameter R""" + + self.P: Parameter[float, Self] = self.add_parameter( + "P", get_cmd="OUTP? 4", get_parser=float, unit="deg" + ) + """Parameter P""" + + self.complex_voltage: Parameter = self.add_parameter( + "complex_voltage", + label="Voltage", + get_cmd=self._get_complex_voltage, + unit="V", + docstring="Complex voltage parameter " + "calculated from X, Y phase using " + "Z = X +j*Y", + vals=ComplexNumbers(), + ) + """Complex voltage parameter calculated from X, Y phase using Z = X +j*Y""" + + # Data buffer settings + self.buffer_SR: Parameter = self.add_parameter( + "buffer_SR", + label="Buffer sample rate", + get_cmd="SRAT ?", + set_cmd=self._set_buffer_SR, + unit="Hz", + val_mapping={ + 62.5e-3: 0, + 0.125: 1, + 0.250: 2, + 0.5: 3, + 1: 4, + 2: 5, + 4: 6, + 8: 7, + 16: 8, + 32: 9, + 64: 10, + 128: 11, + 256: 12, + 512: 13, + "Trigger": 14, + }, + get_parser=int, + ) + """Parameter buffer_SR""" + + self.buffer_acq_mode: Parameter[str, Self] = self.add_parameter( + "buffer_acq_mode", + label="Buffer acquistion mode", + get_cmd="SEND ?", + set_cmd="SEND {}", + val_mapping={"single shot": 0, "loop": 1}, + get_parser=int, + ) + """Parameter buffer_acq_mode""" + + self.buffer_trig_mode: Parameter[str, Self] = self.add_parameter( + "buffer_trig_mode", + label="Buffer trigger start mode", + get_cmd="TSTR ?", + set_cmd="TSTR {}", + val_mapping={"ON": 1, "OFF": 0}, + get_parser=int, + ) + """Parameter buffer_trig_mode""" + + self.buffer_npts: Parameter[int, Self] = self.add_parameter( + "buffer_npts", + label="Buffer number of stored points", + get_cmd="SPTS ?", + get_parser=int, + ) + """Parameter buffer_npts""" + + self.sweep_setpoints: SR830GeneratedSetPoints = self.add_parameter( + "sweep_setpoints", + parameter_class=SR830GeneratedSetPoints, + vals=Arrays(shape=(self.buffer_npts.get,)), + ) + """Parameter sweep_setpoints""" + + # Channel setup + for ch in range(1, 3): + # detailed validation and mapping performed in set/get functions + self.add_parameter( + f"ch{ch}_ratio", + label=f"Channel {ch} ratio", + get_cmd=partial(self._get_ch_ratio, ch), + set_cmd=partial(self._set_ch_ratio, ch), + vals=Strings(), + ) + self.add_parameter( + f"ch{ch}_display", + label=f"Channel {ch} display", + get_cmd=partial(self._get_ch_display, ch), + set_cmd=partial(self._set_ch_display, ch), + vals=Strings(), + ) + self.add_parameter( + f"ch{ch}_databuffer", channel=ch, parameter_class=SR830ChannelBuffer + ) + self.add_parameter( + f"ch{ch}_datatrace", + channel=ch, + vals=Arrays(shape=(self.buffer_npts.get,)), + setpoints=(self.sweep_setpoints,), + parameter_class=SR830ChannelTrace, + ) + + # Auto functions + self.add_function("auto_gain", call_cmd="AGAN") + self.add_function("auto_reserve", call_cmd="ARSV") + self.add_function("auto_phase", call_cmd="APHS") + self.add_function("auto_offset", call_cmd="AOFF {0}", args=[Enum(1, 2, 3)]) + + # Interface + self.add_function("reset", call_cmd="*RST") + + self.add_function("disable_front_panel", call_cmd="OVRM 0") + self.add_function("enable_front_panel", call_cmd="OVRM 1") + + self.add_function( + "send_trigger", + call_cmd="TRIG", + docstring=( + "Send a software trigger. " + "This command has the same effect as a " + "trigger at the rear panel trigger" + " input." + ), + ) + + self.add_function( + "buffer_start", + call_cmd="STRT", + docstring=( + "The buffer_start command starts or " + "resumes data storage. buffer_start" + " is ignored if storage is already in" + " progress." + ), + ) + + self.add_function( + "buffer_pause", + call_cmd="PAUS", + docstring=( + "The buffer_pause command pauses data " + "storage. If storage is already paused " + "or reset then this command is ignored." + ), + ) + + self.add_function( + "buffer_reset", + call_cmd="REST", + docstring=( + "The buffer_reset command resets the data" + " buffers. The buffer_reset command can " + "be sent at any time - any storage in " + "progress, paused or not, will be reset." + " This command will erase the data " + "buffer." + ), + ) + + # Initialize the proper units of the outputs and sensitivities + self.input_config() + + # start keeping track of buffer setpoints + self._buffer1_ready = False + self._buffer2_ready = False + + self.connect_message() + + SNAP_PARAMETERS: ClassVar[dict[str, str]] = { + "x": "1", + "y": "2", + "r": "3", + "p": "4", + "phase": "4", + "θ": "4", + "aux1": "5", + "aux2": "6", + "aux3": "7", + "aux4": "8", + "freq": "9", + "ch1": "10", + "ch2": "11", + } + + def snap(self, *parameters: str) -> tuple[float, ...]: + """ + Get between 2 and 6 parameters at a single instant. This provides a + coherent snapshot of measured signals. Pick up to 6 from: X, Y, R, θ, + the aux inputs 1-4, frequency, or what is currently displayed on + channels 1 and 2. + + Reading X and Y (or R and θ) gives a coherent snapshot of the signal. + Snap is important when the time constant is very short, a time constant + less than 100 ms. + + Args: + *parameters: From 2 to 6 strings of names of parameters for which + the values are requested. including: 'x', 'y', 'r', 'p', + 'phase' or 'θ', 'aux1', 'aux2', 'aux3', 'aux4', 'freq', + 'ch1', and 'ch2'. + + Returns: + A tuple of floating point values in the same order as requested. + + Examples: + >>> lockin.snap('x','y') -> tuple(x,y) + + >>> lockin.snap('aux1','aux2','freq','phase') + >>> -> tuple(aux1,aux2,freq,phase) + + Note: + Volts for x, y, r, and aux 1-4 + Degrees for θ + Hertz for freq + Unknown for ch1 and ch2. It will depend on what was set. + + - If X,Y,R and θ are all read, then the values of X,Y are recorded + approximately 10 µs apart from R,θ. Thus, the values of X and Y + may not yield the exact values of R and θ from a single snap. + - The values of the Aux Inputs may have an uncertainty of + up to 32 µs. + - The frequency is computed only every other period or 40 ms, + whichever is longer. + + """ + if not 2 <= len(parameters) <= 6: + raise KeyError( + "It is only possible to request values of 2 to 6 parameters at a time." + ) + + for name in parameters: + if name.lower() not in self.SNAP_PARAMETERS: + raise KeyError( + f"{name} is an unknown parameter. Refer" + f" to `SNAP_PARAMETERS` for a list of valid" + f" parameter names" + ) + + p_ids = [self.SNAP_PARAMETERS[name.lower()] for name in parameters] + output = self.ask(f"SNAP? {','.join(p_ids)}") + + return tuple(float(val) for val in output.split(",")) + + def increment_sensitivity(self) -> bool: + """ + Increment the sensitivity setting of the lock-in. This is equivalent + to pushing the sensitivity up button on the front panel. This has no + effect if the sensitivity is already at the maximum. + + Returns: + Whether or not the sensitivity was actually changed. + + """ + return self._change_sensitivity(1) + + def decrement_sensitivity(self) -> bool: + """ + Decrement the sensitivity setting of the lock-in. This is equivalent + to pushing the sensitivity down button on the front panel. This has no + effect if the sensitivity is already at the minimum. + + Returns: + Whether or not the sensitivity was actually changed. + + """ + return self._change_sensitivity(-1) + + def _change_sensitivity(self, dn: int) -> bool: + if self.input_config() in ["a", "a-b"]: + n_to = self._N_TO_VOLT + to_n = self._VOLT_TO_N + else: + n_to = self._N_TO_CURR + to_n = self._CURR_TO_N + + n = to_n[self.sensitivity()] + + if n + dn > max(n_to.keys()) or n + dn < min(n_to.keys()): + return False + + self.sensitivity.set(n_to[n + dn]) + return True + + def _set_buffer_SR(self, SR: int) -> None: + self.write(f"SRAT {SR}") + self._buffer1_ready = False + self._buffer2_ready = False + self.sweep_setpoints.update_units_if_constant_sample_rate() + + def _get_ch_ratio(self, channel: int) -> str: + val_mapping = { + 1: {0: "none", 1: "Aux In 1", 2: "Aux In 2"}, + 2: {0: "none", 1: "Aux In 3", 2: "Aux In 4"}, + } + resp = int(self.ask(f"DDEF ? {channel}").split(",")[1]) + + return val_mapping[channel][resp] + + def _set_ch_ratio(self, channel: int, ratio: str) -> None: + val_mapping = { + 1: {"none": 0, "Aux In 1": 1, "Aux In 2": 2}, + 2: {"none": 0, "Aux In 3": 1, "Aux In 4": 2}, + } + vals = val_mapping[channel].keys() + if ratio not in vals: + raise ValueError(f"{ratio} not in {vals}") + ratio_int = val_mapping[channel][ratio] + disp_val = int(self.ask(f"DDEF ? {channel}").split(",")[0]) + self.write(f"DDEF {channel}, {disp_val}, {ratio_int}") + self._buffer_ready = False + + def _get_ch_display(self, channel: int) -> str: + val_mapping = { + 1: {0: "X", 1: "R", 2: "X Noise", 3: "Aux In 1", 4: "Aux In 2"}, + 2: {0: "Y", 1: "Phase", 2: "Y Noise", 3: "Aux In 3", 4: "Aux In 4"}, + } + resp = int(self.ask(f"DDEF ? {channel}").split(",")[0]) + + return val_mapping[channel][resp] + + def _set_ch_display(self, channel: int, disp: str) -> None: + val_mapping = { + 1: {"X": 0, "R": 1, "X Noise": 2, "Aux In 1": 3, "Aux In 2": 4}, + 2: {"Y": 0, "Phase": 1, "Y Noise": 2, "Aux In 3": 3, "Aux In 4": 4}, + } + vals = val_mapping[channel].keys() + if disp not in vals: + raise ValueError(f"{disp} not in {vals}") + disp_int = val_mapping[channel][disp] + # Since ratio AND display are set simultaneously, + # we get and then re-set the current ratio value + ratio_val = int(self.ask(f"DDEF ? {channel}").split(",")[1]) + self.write(f"DDEF {channel}, {disp_int}, {ratio_val}") + self._buffer_ready = False + # we update the unit of the datatrace + # according to the choice of channel + params = self.parameters + dataparam = params[f"ch{channel}_datatrace"] + assert isinstance(dataparam, SR830ChannelTrace) + dataparam.update_unit() + + def _set_units(self, unit: str) -> None: + # TODO: + # make a public parameter function that allows to change the units + for param in [self.X, self.Y, self.R, self.sensitivity]: + param.unit = unit + + def _get_complex_voltage(self) -> complex: + x, y = self.snap("X", "Y") + return x + 1.0j * y + + def _get_input_config(self, s: int) -> str: + mode = self._N_TO_INPUT_CONFIG[int(s)] + + if mode in ["a", "a-b"]: + self.sensitivity.vals = self._VOLT_ENUM + self._set_units("V") + else: + self.sensitivity.vals = self._CURR_ENUM + self._set_units("A") + + return mode + + def _set_input_config(self, s: str) -> int: + if s in ["a", "a-b"]: + self.sensitivity.vals = self._VOLT_ENUM + self._set_units("V") + else: + self.sensitivity.vals = self._CURR_ENUM + self._set_units("A") + + return self._INPUT_CONFIG_TO_N[s] + + def _get_sensitivity(self, s: int) -> float: + if self.input_config() in ["a", "a-b"]: + return self._N_TO_VOLT[int(s)] + else: + return self._N_TO_CURR[int(s)] + + def _set_sensitivity(self, s: float) -> int: + if self.input_config() in ["a", "a-b"]: + return self._VOLT_TO_N[s] + else: + return self._CURR_TO_N[s] + + def autorange(self, max_changes: int = 1) -> None: + """ + Automatically changes the sensitivity of the instrument according to + the R value and defined max_changes. + + Args: + max_changes: Maximum number of steps allowing the function to + automatically change the sensitivity (default is 1). The actual + number of steps needed to change to the optimal sensitivity may + be more or less than this maximum. + + """ + + def autorange_once() -> bool: + r = self.R() + sens = self.sensitivity() + if r > 0.9 * sens: + return self.increment_sensitivity() + elif r < 0.1 * sens: + return self.decrement_sensitivity() + return False + + sets = 0 + while autorange_once() and sets < max_changes: + sets += 1 + time.sleep(self.time_constant()) + + def set_sweep_parameters( + self, + sweep_param: Parameter, + start: float, + stop: float, + n_points: int = 10, + label: str | None = None, + ) -> None: + self.sweep_setpoints.sweep_array = np.linspace(start, stop, n_points) + self.sweep_setpoints.unit = sweep_param.unit + if label is not None: + self.sweep_setpoints.label = label + elif sweep_param.label is not None: + self.sweep_setpoints.label = sweep_param.label + + +class SR830GeneratedSetPoints(Parameter): + """ + A parameter that generates a setpoint array from start, stop and num points + parameters. + """ + + def __init__( + self, + sweep_array: Iterable[float | int] = np.linspace(0, 1, 10), + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + self.sweep_array = sweep_array + self.update_units_if_constant_sample_rate() + + def update_units_if_constant_sample_rate(self) -> None: + """ + If the buffer is filled at a constant sample rate, + update the unit to "s" and label to "Time"; + otherwise do nothing + """ + assert isinstance(self.root_instrument, SR830) + SR = self.root_instrument.buffer_SR.get() + if SR != "Trigger": + self.unit = "s" + self.label = "Time" + + def set_raw(self, value: Iterable[float | int]) -> None: + self.sweep_array = value + + def get_raw(self) -> ParamRawDataType: + assert isinstance(self.root_instrument, SR830) + SR = self.root_instrument.buffer_SR.get() + if SR == "Trigger": + return self.sweep_array + else: + N = self.root_instrument.buffer_npts.get() + dt = 1 / SR + + return np.linspace(0, N * dt, N) diff --git a/src/qcodes/instrument_drivers/stanford_research/_sr860.py b/src/qcodes/instrument_drivers/stanford_research/_sr860.py new file mode 100644 index 000000000000..3efa04b778f8 --- /dev/null +++ b/src/qcodes/instrument_drivers/stanford_research/_sr860.py @@ -0,0 +1,25 @@ +from typing import TYPE_CHECKING + +from qcodes.instrument_drivers.stanford_research._sr86x import SR86x + +if TYPE_CHECKING: + from typing import Unpack + + from qcodes.instrument import VisaInstrumentKWArgs + + +class SR860(SR86x): + """ + QCoDeS driver for the Stanford Research Systems SR860 Lock-in Amplifier. + + The SR860 instrument is almost equal to the SR865, except for the max frequency + """ + + def __init__( + self, + name: str, + address: str, + reset: bool = False, + **kwargs: "Unpack[VisaInstrumentKWArgs]", + ) -> None: + super().__init__(name, address, max_frequency=500e3, reset=reset, **kwargs) diff --git a/src/qcodes/instrument_drivers/stanford_research/_sr865.py b/src/qcodes/instrument_drivers/stanford_research/_sr865.py new file mode 100644 index 000000000000..742d722514c6 --- /dev/null +++ b/src/qcodes/instrument_drivers/stanford_research/_sr865.py @@ -0,0 +1,25 @@ +from typing import TYPE_CHECKING + +from qcodes.instrument_drivers.stanford_research._sr86x import SR86x + +if TYPE_CHECKING: + from typing import Unpack + + from qcodes.instrument import VisaInstrumentKWArgs + + +class SR865(SR86x): + """ + QCoDeS driver for the Stanford Research Systems SR865 Lock-in Amplifier. + + The SR865 instrument is almost equal to the SR860, except for the max frequency + """ + + def __init__( + self, + name: str, + address: str, + reset: bool = False, + **kwargs: "Unpack[VisaInstrumentKWArgs]", + ) -> None: + super().__init__(name, address, max_frequency=2e6, reset=reset, **kwargs) diff --git a/src/qcodes/instrument_drivers/stanford_research/_sr865a.py b/src/qcodes/instrument_drivers/stanford_research/_sr865a.py new file mode 100644 index 000000000000..cb39d51f5238 --- /dev/null +++ b/src/qcodes/instrument_drivers/stanford_research/_sr865a.py @@ -0,0 +1,26 @@ +from typing import TYPE_CHECKING + +from qcodes.instrument_drivers.stanford_research._sr86x import SR86x + +if TYPE_CHECKING: + from typing import Unpack + + from qcodes.instrument import VisaInstrumentKWArgs + + +class SR865A(SR86x): + """ + QCoDeS driver for the Stanford Research Systems SR865A Lock-in Amplifier. + + The SR865A instrument is almost equal to the SR865, except for the + max frequency + """ + + def __init__( + self, + name: str, + address: str, + reset: bool = False, + **kwargs: "Unpack[VisaInstrumentKWArgs]", + ) -> None: + super().__init__(name, address, max_frequency=4e6, reset=reset, **kwargs) diff --git a/src/qcodes/instrument_drivers/stanford_research/_sr86x.py b/src/qcodes/instrument_drivers/stanford_research/_sr86x.py new file mode 100644 index 000000000000..d2bec07c386f --- /dev/null +++ b/src/qcodes/instrument_drivers/stanford_research/_sr86x.py @@ -0,0 +1,1321 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any, ClassVar, Self + +import numpy as np +import numpy.typing as npt + +from qcodes.instrument import ( + ChannelList, + ChannelTuple, + InstrumentBaseKWArgs, + InstrumentChannel, + VisaInstrument, + VisaInstrumentKWArgs, +) +from qcodes.parameters import ArrayParameter +from qcodes.validators import ComplexNumbers, Enum, Ints, Numbers + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from typing import Unpack + + from qcodes.metadatable import SnapshotUpdate + from qcodes.parameters import Parameter + +log = logging.getLogger(__name__) + + +class SR86xBufferReadout(ArrayParameter): + """ + The parameter array that holds read out data. We need this to be compatible + with qcodes.Measure + + Args: + name: Name of the parameter. + instrument: The instrument to add this parameter to. + + """ + + def __init__(self, name: str, instrument: SR86x, **kwargs: Any) -> None: + unit = "deg" + if name in ["X", "Y", "R"]: + unit = "V" + + super().__init__( + name, + shape=(1,), # dummy initial shape + unit=unit, + setpoint_names=("Time",), + setpoint_labels=("Time",), + setpoint_units=("s",), + instrument=instrument, + docstring="Holds an acquired (part of the) data buffer of one channel.", + **kwargs, + ) + + self._capture_data: npt.NDArray | None = None + + def prepare_readout(self, capture_data: npt.NDArray) -> None: + """ + Prepare this parameter for readout. + + Args: + capture_data: The data to capture. + + """ + self._capture_data = capture_data + + data_len = len(capture_data) + self.shape = (data_len,) + self.setpoint_units = ("",) + self.setpoint_names = ("sample_nr",) + self.setpoint_labels = ("Sample number",) + self.setpoints = (tuple(np.arange(0, data_len)),) + + def get_raw(self) -> npt.NDArray: + """ + Public method to access the capture data + """ + if self._capture_data is None: + raise ValueError( + f"Cannot return data for parameter {self.name}. " + f"Please prepare for readout by calling " + f"'get_capture_data' with appropriate " + f"configuration settings" + ) + + return self._capture_data + + +class SR86xBuffer(InstrumentChannel["SR86x"]): + """ + Buffer module for the SR86x drivers. + + This driver has been verified to work with the SR860 and SR865. + For reference, please consult the SR860 + manual: http://thinksrs.com/downloads/PDFs/Manuals/SR860m.pdf + """ + + def __init__( + self, parent: SR86x, name: str, **kwargs: Unpack[InstrumentBaseKWArgs] + ) -> None: + super().__init__(parent, name, **kwargs) + + self.capture_length_in_kb: Parameter[int, Self] = self.add_parameter( + "capture_length_in_kb", + label="get/set capture length", + get_cmd="CAPTURELEN?", + set_cmd="CAPTURELEN {}", + set_parser=self._set_capture_len_parser, + get_parser=int, + unit="kB", + ) + """Parameter capture_length_in_kb""" + self.bytes_per_sample = 4 + self.min_capture_length_in_kb = 1 # i.e. minimum buffer size + self.max_capture_length_in_kb = 4096 # i.e. maximum buffer size + # Maximum amount of kB that can be read per single CAPTUREGET command + self.max_size_per_reading_in_kb = 64 + + self.capture_config: Parameter[str, Self] = self.add_parameter( + "capture_config", + label="capture configuration", + get_cmd="CAPTURECFG?", + set_cmd="CAPTURECFG {}", + val_mapping={"X": "0", "X,Y": "1", "R,T": "2", "X,Y,R,T": "3"}, + ) + """Parameter capture_config configures which parameters we want to capture""" + + self.capture_rate_max: Parameter[float, Self] = self.add_parameter( + "capture_rate_max", + label="capture rate maximum", + get_cmd="CAPTURERATEMAX?", + get_parser=float, + ) + """Parameter capture_rate_max""" + + self.capture_rate: Parameter[float, Self] = self.add_parameter( + "capture_rate", + label="capture rate raw", + get_cmd="CAPTURERATE?", + set_cmd="CAPTURERATE {}", + get_parser=float, + set_parser=self._set_capture_rate_parser, + ) + """Parameter capture_rate""" + + max_rate = self.capture_rate_max() + self.available_frequencies = [max_rate / 2**i for i in range(20)] + + self.capture_status: Parameter[str, Self] = self.add_parameter( + "capture_status", label="capture status", get_cmd="CAPTURESTAT?" + ) + """Parameter capture_status: Are we capturing at the moment?""" + + self.count_capture_bytes: Parameter[int, Self] = self.add_parameter( + "count_capture_bytes", + label="captured bytes", + get_cmd="CAPTUREBYTES?", + unit="B", + get_parser=int, + docstring="Number of bytes captured so far in the buffer. Can be " + "used to track live progress.", + ) + """Number of bytes captured so far in the buffer. Can be used to track live progress.""" + + self.count_capture_kilobytes: Parameter[int, Self] = self.add_parameter( + "count_capture_kilobytes", + label="captured kilobytes", + get_cmd="CAPTUREPROG?", + get_parser=int, + unit="kB", + docstring="Number of kilobytes captured so far in the buffer, " + "rounded-up to 2 kilobyte chunks. Capture must be " + "stopped before requesting the value of this " + "parameter. If the acquisition wrapped during operating " + "in Continuous mode, then the returned value is " + "simply equal to the current capture length.", + ) + """ + Number of kilobytes captured so far in the buffer, rounded-up to 2 kilobyte chunks. + Capture must be stopped before requesting the value of this parameter. + If the acquisition wrapped during operating in Continuous mode, + then the returned value is simply equal to the current capture length. + """ + + self.X: SR86xBufferReadout = self.add_parameter( + "X", parameter_class=SR86xBufferReadout + ) + """ + X buffer readout. + """ + self.Y: SR86xBufferReadout = self.add_parameter( + "Y", parameter_class=SR86xBufferReadout + ) + """ + Y buffer readout. + """ + self.R: SR86xBufferReadout = self.add_parameter( + "R", parameter_class=SR86xBufferReadout + ) + """ + R buffer readout. + """ + self.T: SR86xBufferReadout = self.add_parameter( + "T", parameter_class=SR86xBufferReadout + ) + """ + T buffer readout. + """ + + def snapshot_base( + self, + update: bool | SnapshotUpdate | None = "Only_invalid", + params_to_skip_update: Sequence[str] | None = None, + ) -> dict[Any, Any]: + if params_to_skip_update is None: + params_to_skip_update = [] + # we omit count_capture_kilobytes from the snapshot because + # it can only be read after a completed capture and will + # timeout otherwise when the snapshot is updated, e.g. at + # station creation time + params_to_skip_update = list(params_to_skip_update) + params_to_skip_update.append("count_capture_kilobytes") + + snapshot = super().snapshot_base(update, params_to_skip_update) + return snapshot + + def _set_capture_len_parser(self, capture_length_in_kb: int) -> int: + """ + Parse the capture length in kB according to the way buffer treats it + (refer to the manual for details). The given value has to fit in the + range and has to be even, otherwise this function raises exceptions. + + Args: + capture_length_in_kb: The desired capture length in kB. + + Returns: + capture_length_in_kb + + """ + if capture_length_in_kb % 2: + raise ValueError("The capture length should be an even number") + + if ( + not self.min_capture_length_in_kb + <= capture_length_in_kb + <= self.max_capture_length_in_kb + ): + raise ValueError( + f"The capture length should be between " + f"{self.min_capture_length_in_kb} and " + f"{self.max_capture_length_in_kb}" + ) + + return capture_length_in_kb + + def set_capture_rate_to_maximum(self) -> None: + """ + Sets the capture rate to maximum. The maximum capture rate is + retrieved from the device, and depends on the current value of the + time constant. + """ + self.capture_rate(self.capture_rate_max()) + + def _set_capture_rate_parser(self, capture_rate_hz: float) -> int: + """ + According to the manual, the capture rate query returns a value in + Hz, but then setting this value it is expected to give a value n, + where the capture rate in Hz is given by + capture_rate_hz = max_rate / 2 ** n. Please see page 136 of the + manual. Here n is an integer in the range [0, 20]. + + Args: + capture_rate_hz: The desired capture rate in Hz. If the desired + rate is more than 1 Hz from the nearest valid rate, a warning + is issued and the nearest valid rate it used. + + Returns: + n_round + + """ + max_rate = self.capture_rate_max() + n = np.log2(max_rate / capture_rate_hz) + n_round = round(n) + + if not 0 <= n_round <= 20: + raise ValueError( + f"The chosen frequency is invalid. Please " + f"consult the SR860 manual at page 136. " + f"The maximum capture rate is {max_rate}" + ) + + nearest_valid_rate = max_rate / 2**n_round + if abs(capture_rate_hz - nearest_valid_rate) > 1: + available_frequencies = ", ".join( + str(f) for f in self.available_frequencies + ) + log.warning(f"Warning: Setting capture rate to {nearest_valid_rate:.5} Hz") + log.warning(f"The available frequencies are: {available_frequencies}") + + return n_round + + def start_capture(self, acquisition_mode: str, trigger_mode: str) -> None: + """ + Start an acquisition. Please see page 137 of the manual for a detailed + explanation. + + Args: + acquisition_mode: "ONE" | "CONT" + trigger_mode: "IMM" | "TRIG" | "SAMP" + + """ + + if acquisition_mode not in ["ONE", "CONT"]: + raise ValueError("The acquisition mode needs to be either 'ONE' or 'CONT'") + + if trigger_mode not in ["IMM", "TRIG", "SAMP"]: + raise ValueError( + "The trigger mode needs to be either 'IMM', 'TRIG' or 'SAMP'" + ) + + cmd_str = f"CAPTURESTART {acquisition_mode}, {trigger_mode}" + self.write(cmd_str) + + def stop_capture(self) -> None: + """Stop a capture""" + self.write("CAPTURESTOP") + + def _get_list_of_capture_variable_names(self) -> list[str]: + """ + Retrieve the list of names of variables (readouts) that are + set to be captured + """ + return self.capture_config().split(",") + + def _get_number_of_capture_variables(self) -> int: + """ + Retrieve the number of variables (readouts) that are + set to be captured + """ + capture_variables = self._get_list_of_capture_variable_names() + n_variables = len(capture_variables) + return n_variables + + def _calc_capture_size_in_kb(self, sample_count: int) -> int: + """ + Given the number of samples to capture, calculate the capture length + that the buffer needs to be set to in order to fit the requested + number of samples. Note that the number of activated readouts is + taken into account. + """ + n_variables = self._get_number_of_capture_variables() + total_size_in_kb = int( + np.ceil(n_variables * sample_count * self.bytes_per_sample / 1024) + ) + # Make sure that the total size in kb is an even number, as expected by + # the instrument + if total_size_in_kb % 2: + total_size_in_kb += 1 + return total_size_in_kb + + def set_capture_length_to_fit_samples(self, sample_count: int) -> None: + """ + Set the capture length of the buffer to fit the given number of + samples. + + Args: + sample_count: Number of samples that the buffer has to fit + + """ + total_size_in_kb = self._calc_capture_size_in_kb(sample_count) + self.capture_length_in_kb(total_size_in_kb) + + def wait_until_samples_captured(self, sample_count: int) -> None: + """ + Wait until the given number of samples is captured. This function + is blocking and has to be used with caution because it does not have + a timeout. + + Args: + sample_count: Number of samples that needs to be captured + + """ + n_captured_bytes = 0 + n_variables = self._get_number_of_capture_variables() + n_bytes_to_capture = sample_count * n_variables * self.bytes_per_sample + while n_captured_bytes < n_bytes_to_capture: + n_captured_bytes = self.count_capture_bytes() + + def get_capture_data(self, sample_count: int) -> dict[str, npt.NDArray]: + """ + Read the given number of samples of the capture data from the buffer. + + Args: + sample_count: number of samples to read from the buffer + + Returns: + The keys in the dictionary correspond to the captured + variables. For instance, if before the capture, the capture + config was set as 'capture_config("X,Y")', then the keys will + be "X" and "Y". The values in the dictionary are numpy arrays + of numbers. + + """ + total_size_in_kb = self._calc_capture_size_in_kb(sample_count) + capture_variables = self._get_list_of_capture_variable_names() + n_variables = self._get_number_of_capture_variables() + + values = self._get_raw_capture_data(total_size_in_kb) + + # Remove zeros which mark the end part of the buffer that is not + # filled with captured data + values = values[values != 0] + + values = values.reshape((-1, n_variables)).T + values = values[:, :sample_count] + + data = {k: v for k, v in zip(capture_variables, values)} + + for capture_variable in capture_variables: + buffer_parameter = getattr(self, capture_variable) + buffer_parameter.prepare_readout(data[capture_variable]) + + return data + + def _get_raw_capture_data(self, size_in_kb: int) -> npt.NDArray: + """ + Read data from the buffer from its beginning avoiding the instrument + limit of 64 kilobytes per reading. + + Args: + size_in_kb :Size of the data that needs to be read; if it exceeds + the capture length, an exception is raised. + + Returns: + A one-dimensional numpy array of the requested data. Note that the + returned array contains data for all the variables that are + mentioned in the capture config. + + """ + current_capture_length = self.capture_length_in_kb() + if size_in_kb > current_capture_length: + raise ValueError( + f"The size of the requested data ({size_in_kb}kB) " + f"is larger than current capture length of the " + f"buffer ({current_capture_length}kB)." + ) + + values: npt.NDArray = np.array([]) + data_size_to_read_in_kb = size_in_kb + n_readings = 0 + + while data_size_to_read_in_kb > 0: + offset = n_readings * self.max_size_per_reading_in_kb + + if data_size_to_read_in_kb > self.max_size_per_reading_in_kb: + size_of_this_reading = self.max_size_per_reading_in_kb + else: + size_of_this_reading = data_size_to_read_in_kb + + data_from_this_reading = self._get_raw_capture_data_block( + size_of_this_reading, offset_in_kb=offset + ) + values = np.append(values, data_from_this_reading) + + data_size_to_read_in_kb -= size_of_this_reading + n_readings += 1 + + return values + + def _get_raw_capture_data_block( + self, size_in_kb: int, offset_in_kb: int = 0 + ) -> npt.NDArray: + """ + Read data from the buffer. The maximum amount of data that can be + read with this function (size_in_kb) is 64kB (this limitation comes + from the instrument). The offset argument can be used to navigate + along the buffer. + + An exception will be raised if either size_in_kb or offset_in_kb are + longer that the *current* capture length (number of kB of data that is + captured so far rounded up to 2kB chunks). If (offset_in_kb + + size_in_kb) is longer than the *current* capture length, + the instrument returns the wrapped data. + + For more information, refer to the description of the "CAPTUREGET" + command in the manual. + + Args: + size_in_kb: Amount of data in kB that is to be read from the buffer + offset_in_kb: Offset within the buffer of where to read the data; + for example, when 0 is specified, the data is read from the + start of the buffer. + + Returns: + A one-dimensional numpy array of the requested data. Note that the + returned array contains data for all the variables that are + mentioned in the capture config. + + """ + if size_in_kb > self.max_size_per_reading_in_kb: + raise ValueError( + f"The size of the requested data ({size_in_kb}kB) " + f"is larger than maximum size that can be read " + f"at once ({self.max_size_per_reading_in_kb}kB)." + ) + + # Calculate the size of the data captured so far, in kB, rounded up + # to 2kB chunks + size_of_currently_captured_data = int( + np.ceil(np.ceil(self.count_capture_bytes() / 1024) / 2) * 2 + ) + + if size_in_kb > size_of_currently_captured_data: + raise ValueError( + f"The size of the requested data ({size_in_kb}kB) " + f"cannot be larger than the size of currently " + f"captured data rounded up to 2kB chunks " + f"({size_of_currently_captured_data}kB)" + ) + + if offset_in_kb > size_of_currently_captured_data: + raise ValueError( + f"The offset for reading the requested data " + f"({offset_in_kb}kB) cannot be larger than the " + f"size of currently captured data rounded up to " + f"2kB chunks " + f"({size_of_currently_captured_data}kB)" + ) + + values = self.parent.visa_handle.query_binary_values( + f"CAPTUREGET? {offset_in_kb}, {size_in_kb}", + datatype="f", + is_big_endian=False, + expect_termination=False, + ) + # the sr86x does not include an extra termination char on binary + # messages so we set expect_termination to False + + return np.array(values) + + def capture_one_sample_per_trigger( + self, trigger_count: int, start_triggers_pulsetrain: Callable[..., Any] + ) -> dict[str, npt.NDArray]: + """ + Capture one sample per each trigger, and return when the specified + number of triggers has been received. + + Args: + trigger_count: Number of triggers to capture samples for + start_triggers_pulsetrain: By calling this *non-blocking* + function, the train of trigger pulses should start + + Returns: + The keys in the dictionary correspond to the captured + variables. For instance, if before the capture, the capture + config was set as 'capture_config("X,Y")', then the keys will + be "X" and "Y". The values in the dictionary are numpy arrays + of numbers. + + """ + self.set_capture_length_to_fit_samples(trigger_count) + self.start_capture("ONE", "SAMP") + start_triggers_pulsetrain() + self.wait_until_samples_captured(trigger_count) + self.stop_capture() + return self.get_capture_data(trigger_count) + + def capture_samples_after_trigger( + self, sample_count: int, send_trigger: Callable[..., Any] + ) -> dict[str, npt.NDArray]: + """ + Capture a number of samples after a trigger has been received. + Please refer to page 135 of the manual for details. + + Args: + sample_count: Number of samples to capture + send_trigger: By calling this *non-blocking* function, one trigger + should be sent that will initiate the capture + + Returns: + The keys in the dictionary correspond to the captured + variables. For instance, if before the capture, the capture + config was set as 'capture_config("X,Y")', then the keys will + be "X" and "Y". The values in the dictionary are numpy arrays + of numbers. + + """ + self.set_capture_length_to_fit_samples(sample_count) + self.start_capture("ONE", "TRIG") + send_trigger() + self.wait_until_samples_captured(sample_count) + self.stop_capture() + return self.get_capture_data(sample_count) + + def capture_samples(self, sample_count: int) -> dict[str, npt.NDArray]: + """ + Capture a number of samples at a capture rate, starting immediately. + Unlike the "continuous" capture mode, here the buffer does not get + overwritten with the new data once the buffer is full. + + The function blocks until the required number of samples is acquired, + and returns them. + + Args: + sample_count: Number of samples to capture + + Returns: + The keys in the dictionary correspond to the captured + variables. For instance, if before the capture, the capture + config was set as 'capture_config("X,Y")', then the keys will + be "X" and "Y". The values in the dictionary are numpy arrays + of numbers. + + """ + self.set_capture_length_to_fit_samples(sample_count) + self.start_capture("ONE", "IMM") + self.wait_until_samples_captured(sample_count) + self.stop_capture() + return self.get_capture_data(sample_count) + + +class SR86xDataChannel(InstrumentChannel["SR86x"]): + """ + Implements a data channel of SR86x lock-in amplifier. Parameters that are + assigned to these channels get plotted on the display of the instrument. + Moreover, there are commands that allow to conveniently retrieve the values + of the parameters that are currently assigned to the data channels. + + This class relies on the available parameter names that should be + mentioned in the lock-in amplifier class in `PARAMETER_NAMES` attribute. + + Args: + parent: an instance of SR86x driver + name: data channel name that is to be used to reference it from the + parent + cmd_id: this ID is used in VISA commands to refer to this data channel, + usually is an integer number + cmd_id_name: this name can also be used in VISA commands along with + channel_id; it is not used in this implementation, but is added + for reference + color: every data channel is also referred to by the color with which it + is being plotted on the instrument's screen; added here only for + reference + + """ + + def __init__( + self, + parent: SR86x, + name: str, + cmd_id: str, + cmd_id_name: str | None = None, + color: str | None = None, + **kwargs: Unpack[InstrumentBaseKWArgs], + ) -> None: + super().__init__(parent, name, **kwargs) + + self._cmd_id = cmd_id + self._cmd_id_name = cmd_id_name + self._color = color + + self.assigned_parameter: Parameter[str, Self] = self.add_parameter( + "assigned_parameter", + label=f"Data channel {cmd_id} parameter", + docstring=f"Allows to set and get the " + f"parameter that is assigned to data " + f"channel {cmd_id}", + set_cmd=f"CDSP {cmd_id}, {{}}", + get_cmd=f"CDSP? {cmd_id}", + val_mapping=self.parent.PARAMETER_NAMES, + ) + """Allows to set and get the parameter that is assigned to the channel""" + + @property + def cmd_id(self) -> str: + return self._cmd_id + + @property + def cmd_id_name(self) -> str | None: + return self._cmd_id_name + + @property + def color(self) -> str | None: + return self._color + + +class SR86x(VisaInstrument): + """ + Base class for Stanford SR86x Lock-in Amplifier drivers. This class should not + be instantiated directly instead one of the model specific sub classes should be used. + """ + + _VOLT_TO_N: ClassVar[dict[int | float, int]] = { + 1: 0, + 500e-3: 1, + 200e-3: 2, + 100e-3: 3, + 50e-3: 4, + 20e-3: 5, + 10e-3: 6, + 5e-3: 7, + 2e-3: 8, + 1e-3: 9, + 500e-6: 10, + 200e-6: 11, + 100e-6: 12, + 50e-6: 13, + 20e-6: 14, + 10e-6: 15, + 5e-6: 16, + 2e-6: 17, + 1e-6: 18, + 500e-9: 19, + 200e-9: 20, + 100e-9: 21, + 50e-9: 22, + 20e-9: 23, + 10e-9: 24, + 5e-9: 25, + 2e-9: 26, + 1e-9: 27, + } + _N_TO_VOLT: ClassVar[dict[int, int | float]] = {v: k for k, v in _VOLT_TO_N.items()} + + _CURR_TO_N: ClassVar[dict[float, int]] = { + 1e-6: 0, + 500e-9: 1, + 200e-9: 2, + 100e-9: 3, + 50e-9: 4, + 20e-9: 5, + 10e-9: 6, + 5e-9: 7, + 2e-9: 8, + 1e-9: 9, + 500e-12: 10, + 200e-12: 11, + 100e-12: 12, + 50e-12: 13, + 20e-12: 14, + 10e-12: 15, + 5e-12: 16, + 2e-12: 17, + 1e-12: 18, + 500e-15: 19, + 200e-15: 20, + 100e-15: 21, + 50e-15: 22, + 20e-15: 23, + 10e-15: 24, + 5e-15: 25, + 2e-15: 26, + 1e-15: 27, + } + _N_TO_CURR: ClassVar[dict[int, float]] = {v: k for k, v in _CURR_TO_N.items()} + + _VOLT_ENUM = Enum(*_VOLT_TO_N.keys()) + _CURR_ENUM = Enum(*_CURR_TO_N.keys()) + + _INPUT_SIGNAL_TO_N: ClassVar[dict[str, int]] = { + "voltage": 0, + "current": 1, + } + _N_TO_INPUT_SIGNAL: ClassVar[dict[int, str]] = { + v: k for k, v in _INPUT_SIGNAL_TO_N.items() + } + + PARAMETER_NAMES: ClassVar[dict[str, str]] = { + "X": "0", # X output, 'X' + "Y": "1", # Y output, 'Y' + "R": "2", # R output, 'R' + "P": "3", # theta output, 'THeta' + "aux_in1": "4", # Aux In 1, 'IN1' + "aux_in2": "5", # Aux In 2, 'IN2' + "aux_in3": "6", # Aux In 3, 'IN3' + "aux_in4": "7", # Aux In 4, 'IN4' + "Xnoise": "8", # X noise, 'XNOise' + "Ynoise": "9", # Y noise, 'YNOise' + "aux_out1": "10", # Aux Out 1, 'OUT1' + "aux_out2": "11", # Aux Out 2, 'OUT2' + "phase": "12", # Reference Phase, 'PHAse' + "amplitude": "13", # Sine Out Amplitude, 'SAMp' + "sine_outdc": "14", # DC Level, 'LEVel' + "frequency": "15", # Int. Ref. Frequency, 'FInt' + "frequency_ext": "16", # Ext. Ref. Frequency, 'FExt' + } + + _N_DATA_CHANNELS = 4 + + default_terminator = "\n" + + def __init__( + self, + name: str, + address: str, + max_frequency: float, + reset: bool = False, + **kwargs: Unpack[VisaInstrumentKWArgs], + ): + super().__init__(name, address, **kwargs) + self._max_frequency = max_frequency + # Reference commands + self.frequency: Parameter[float, Self] = self.add_parameter( + name="frequency", + label="Frequency", + unit="Hz", + get_cmd="FREQ?", + set_cmd="FREQ {}", + get_parser=float, + vals=Numbers(min_value=1e-3, max_value=self._max_frequency), + ) + """Parameter frequency""" + self.sine_outdc: Parameter[float, Self] = self.add_parameter( + name="sine_outdc", + label="Sine out dc level", + unit="V", + get_cmd="SOFF?", + set_cmd="SOFF {}", + get_parser=float, + vals=Numbers(min_value=-5, max_value=5), + ) + """Parameter sine_outdc""" + self.amplitude: Parameter[float, Self] = self.add_parameter( + name="amplitude", + label="Amplitude", + unit="V", + get_cmd="SLVL?", + set_cmd="SLVL {}", + get_parser=float, + vals=Numbers(min_value=0, max_value=2), + ) + """Parameter amplitude""" + self.harmonic: Parameter[int, Self] = self.add_parameter( + name="harmonic", + label="Harmonic", + get_cmd="HARM?", + get_parser=int, + set_cmd="HARM {:d}", + vals=Ints(min_value=1, max_value=99), + ) + """Parameter harmonic""" + self.phase: Parameter[float, Self] = self.add_parameter( + name="phase", + label="Phase", + unit="deg", + get_cmd="PHAS?", + set_cmd="PHAS {}", + get_parser=float, + vals=Numbers(min_value=-3.6e5, max_value=3.6e5), + ) + """Parameter phase""" + # Signal commands + self.sensitivity: Parameter[float, Self] = self.add_parameter( + name="sensitivity", + label="Sensitivity", + get_cmd="SCAL?", + set_cmd="SCAL {:d}", + get_parser=self._get_sensitivity, + set_parser=self._set_sensitivity, + ) + """Parameter sensitivity""" + self.filter_slope: Parameter[int, Self] = self.add_parameter( + name="filter_slope", + label="Filter slope", + unit="dB/oct", + get_cmd="OFSL?", + set_cmd="OFSL {}", + val_mapping={6: 0, 12: 1, 18: 2, 24: 3}, + ) + """Parameter filter_slope""" + self.sync_filter: Parameter[str, Self] = self.add_parameter( + name="sync_filter", + label="Sync filter", + get_cmd="SYNC?", + set_cmd="SYNC {}", + val_mapping={"OFF": 0, "ON": 1}, + ) + """Parameter sync_filter""" + self.noise_bandwidth: Parameter[float, Self] = self.add_parameter( + name="noise_bandwidth", + label="Noise bandwidth", + unit="Hz", + get_cmd="ENBW?", + get_parser=float, + ) + """Parameter noise_bandwidth""" + self.signal_strength: Parameter[int, Self] = self.add_parameter( + name="signal_strength", + label="Signal strength indicator", + get_cmd="ILVL?", + get_parser=int, + ) + """Parameter signal_strength""" + self.signal_input: Parameter[str, Self] = self.add_parameter( + name="signal_input", + label="Signal input", + get_cmd="IVMD?", + get_parser=self._get_input_config, + set_cmd="IVMD {}", + set_parser=self._set_input_config, + vals=Enum(*self._INPUT_SIGNAL_TO_N.keys()), + ) + """Parameter signal_input""" + self.input_range: Parameter = self.add_parameter( + name="input_range", + label="Input range", + unit="V", + get_cmd="IRNG?", + set_cmd="IRNG {}", + val_mapping={1: 0, 300e-3: 1, 100e-3: 2, 30e-3: 3, 10e-3: 4}, + ) + """Parameter input_range""" + self.input_config: Parameter[str, Self] = self.add_parameter( + name="input_config", + label="Input configuration", + get_cmd="ISRC?", + set_cmd="ISRC {}", + val_mapping={"a": 0, "a-b": 1}, + ) + """Parameter input_config""" + self.input_shield: Parameter[str, Self] = self.add_parameter( + name="input_shield", + label="Input shield", + get_cmd="IGND?", + set_cmd="IGND {}", + val_mapping={"float": 0, "ground": 1}, + ) + """Parameter input_shield""" + self.input_gain: Parameter[float, Self] = self.add_parameter( + name="input_gain", + label="Input gain", + unit="ohm", + get_cmd="ICUR?", + set_cmd="ICUR {}", + val_mapping={1e6: 0, 100e6: 1}, + ) + """Parameter input_gain""" + self.adv_filter: Parameter[str, Self] = self.add_parameter( + name="adv_filter", + label="Advanced filter", + get_cmd="ADVFILT?", + set_cmd="ADVFILT {}", + val_mapping={"OFF": 0, "ON": 1}, + ) + """Parameter adv_filter""" + self.input_coupling: Parameter[str, Self] = self.add_parameter( + name="input_coupling", + label="Input coupling", + get_cmd="ICPL?", + set_cmd="ICPL {}", + val_mapping={"ac": 0, "dc": 1}, + ) + """Parameter input_coupling""" + self.time_constant: Parameter = self.add_parameter( + name="time_constant", + label="Time constant", + unit="s", + get_cmd="OFLT?", + set_cmd="OFLT {}", + val_mapping={ + 1e-6: 0, + 3e-6: 1, + 10e-6: 2, + 30e-6: 3, + 100e-6: 4, + 300e-6: 5, + 1e-3: 6, + 3e-3: 7, + 10e-3: 8, + 30e-3: 9, + 100e-3: 10, + 300e-3: 11, + 1: 12, + 3: 13, + 10: 14, + 30: 15, + 100: 16, + 300: 17, + 1e3: 18, + 3e3: 19, + 10e3: 20, + 30e3: 21, + }, + ) + """Parameter time_constant""" + + self.external_reference_trigger: Parameter[str, Self] = self.add_parameter( + name="external_reference_trigger", + label="External reference trigger mode", + get_cmd="RTRG?", + set_cmd="RTRG {}", + val_mapping={ + "SIN": 0, + "POS": 1, + "POSTTL": 1, + "NEG": 2, + "NEGTTL": 2, + }, + docstring="The triggering mode for synchronization of the " + "internal reference signal with the externally provided " + "one", + ) + """The triggering mode for synchronization of the internal reference signal with the externally provided one""" + + self.reference_source: Parameter[str, Self] = self.add_parameter( + name="reference_source", + label="Reference source", + get_cmd="RSRC?", + set_cmd="RSRC {}", + val_mapping={"INT": 0, "EXT": 1, "DUAL": 2, "CHOP": 3}, + docstring="The source of the reference signal", + ) + """The source of the reference signal""" + + self.external_reference_trigger_input_resistance: Parameter = ( + self.add_parameter( + name="external_reference_trigger_input_resistance", + label="External reference trigger input resistance", + get_cmd="REFZ?", + set_cmd="REFZ {}", + val_mapping={ + "50": 0, + "50OHMS": 0, + 0: 0, + "1M": 1, + "1MEG": 1, + 1: 1, + }, + docstring="Input resistance of the input for the external " + "reference signal", + ) + ) + """Input resistance of the input for the external reference signal""" + + # Auto functions + self.add_function("auto_range", call_cmd="ARNG") + self.add_function("auto_scale", call_cmd="ASCL") + self.add_function("auto_phase", call_cmd="APHS") + + # Data transfer + # first 4 parameters from a list of 16 below. + self.X: Parameter[float, Self] = self.add_parameter( + "X", + label="In-phase Magnitude", + get_cmd="OUTP? 0", + get_parser=float, + unit="V", + ) + """Parameter X""" + self.Y: Parameter[float, Self] = self.add_parameter( + "Y", + label="Out-phase Magnitude", + get_cmd="OUTP? 1", + get_parser=float, + unit="V", + ) + """Parameter Y""" + self.R: Parameter[float, Self] = self.add_parameter( + "R", label="Magnitude", get_cmd="OUTP? 2", get_parser=float, unit="V" + ) + """Parameter R""" + self.P: Parameter[float, Self] = self.add_parameter( + "P", label="Phase", get_cmd="OUTP? 3", get_parser=float, unit="deg" + ) + """Parameter P""" + + self.complex_voltage: Parameter = self.add_parameter( + "complex_voltage", + label="Voltage", + get_cmd=self._get_complex_voltage, + unit="V", + vals=ComplexNumbers(), + ) + """Parameter complex_voltage""" + + # CH1/CH2 Output Commands + self.X_offset: Parameter[float, Self] = self.add_parameter( + "X_offset", + label="X offset ", + unit="%", + get_cmd="COFP? 0", + set_cmd="COFP 0, {}", + get_parser=float, + vals=Numbers(min_value=-999.99, max_value=999.99), + ) + """Parameter X_offset""" + self.Y_offset: Parameter[float, Self] = self.add_parameter( + "Y_offset", + label="Y offset", + unit="%", + get_cmd="COFP? 1", + set_cmd="COFP 1, {}", + get_parser=float, + vals=Numbers(min_value=-999.99, max_value=999.99), + ) + """Parameter Y_offset""" + self.R_offset: Parameter[float, Self] = self.add_parameter( + "R_offset", + label="R offset", + unit="%", + get_cmd="COFP? 2", + set_cmd="COFP 2, {}", + get_parser=float, + vals=Numbers(min_value=-999.99, max_value=999.99), + ) + """Parameter R_offset""" + self.X_expand: Parameter[str, Self] = self.add_parameter( + "X_expand", + label="X expand multiplier", + get_cmd="CEXP? 0", + set_cmd="CEXP 0, {}", + val_mapping={"OFF": "0", "X10": "1", "X100": "2"}, + ) + """Parameter X_expand""" + self.Y_expand: Parameter[str, Self] = self.add_parameter( + "Y_expand", + label="Y expand multiplier", + get_cmd="CEXP? 1", + set_cmd="CEXP 1, {}", + val_mapping={"OFF": 0, "X10": 1, "X100": 2}, + ) + """Parameter Y_expand""" + self.R_expand: Parameter[str, Self] = self.add_parameter( + "R_expand", + label="R expand multiplier", + get_cmd="CEXP? 2", + set_cmd="CEXP 2, {}", + val_mapping={"OFF": 0, "X10": 1, "X100": 2}, + ) + """Parameter R_expand""" + + # Aux input/output + for i in [0, 1, 2, 3]: + self.add_parameter( + f"aux_in{i}", + label=f"Aux input {i}", + get_cmd=f"OAUX? {i}", + get_parser=float, + unit="V", + ) + self.add_parameter( + f"aux_out{i}", + label=f"Aux output {i}", + get_cmd=f"AUXV? {i}", + get_parser=float, + set_cmd=f"AUXV {i}, {{}}", + unit="V", + ) + + # Data channels: + # 'DAT1' (green), 'DAT2' (blue), 'DAT3' (yellow), 'DAT4' (orange) + data_channels = ChannelList( + self, "data_channels", SR86xDataChannel, snapshotable=False + ) + for num, color in zip( + range(self._N_DATA_CHANNELS), ("green", "blue", "yellow", "orange") + ): + cmd_id = f"{num}" + cmd_id_name = f"DAT{num + 1}" + ch_name = f"data_channel_{num + 1}" + + data_channel = SR86xDataChannel(self, ch_name, cmd_id, cmd_id_name, color) + + data_channels.append(data_channel) + self.add_submodule(ch_name, data_channel) + + self.data_channels: ChannelTuple[SR86xDataChannel] = self.add_submodule( + "data_channels", data_channels.to_channel_tuple() + ) + """Interface for the SR86x data channels""" + + # Interface + self.add_function("reset", call_cmd="*RST") + + self.add_function("disable_front_panel", call_cmd="OVRM 0") + self.add_function("enable_front_panel", call_cmd="OVRM 1") + + buffer = SR86xBuffer(self, f"{self.name}_buffer") + self.buffer: SR86xBuffer = self.add_submodule("buffer", buffer) + """Interface for the SR86x buffer""" + + self.input_config() + self.connect_message() + + def _set_units(self, unit: str) -> None: + for param in [self.X, self.Y, self.R, self.sensitivity]: + param.unit = unit + + def _get_complex_voltage(self) -> complex: + x, y = self.get_values("X", "Y") + return x + 1.0j * y + + def _get_input_config(self, s: int) -> str: + mode = self._N_TO_INPUT_SIGNAL[int(s)] + + if mode == "voltage": + self.sensitivity.vals = self._VOLT_ENUM + self._set_units("V") + else: + self.sensitivity.vals = self._CURR_ENUM + self._set_units("A") + + return mode + + def _set_input_config(self, s: str) -> int: + if s == "voltage": + self.sensitivity.vals = self._VOLT_ENUM + self._set_units("V") + else: + self.sensitivity.vals = self._CURR_ENUM + self._set_units("A") + + return self._INPUT_SIGNAL_TO_N[s] + + def _get_sensitivity(self, s: int) -> float: + if self.signal_input() == "voltage": + return self._N_TO_VOLT[int(s)] + else: + return self._N_TO_CURR[int(s)] + + def _set_sensitivity(self, s: float) -> int: + if self.signal_input() == "voltage": + return self._VOLT_TO_N[s] + else: + return self._CURR_TO_N[s] + + def get_values(self, *parameter_names: str) -> tuple[float, ...]: + """ + Get values of 2 or 3 parameters that are measured by the lock-in + amplifier. These values are guaranteed to come from the same + measurement cycle as opposed to getting values of parameters one by + one (for example, by calling `sr.X()`, and then `sr.Y()`. + + Args: + *parameter_names: 2 or 3 names of parameters for which the values + are requested; valid names can be found in `PARAMETER_NAMES` + attribute of the driver class + + Returns: + a tuple of 2 or 3 floating point values + + """ + if not 2 <= len(parameter_names) <= 3: + raise KeyError( + "It is only possible to request values of 2 or 3 parameters at a time." + ) + + for name in parameter_names: + if name not in self.PARAMETER_NAMES: + raise KeyError( + f"{name} is not a valid parameter name. Refer " + f"to `PARAMETER_NAMES` for a list of valid " + f"parameter names" + ) + + p_ids = [self.PARAMETER_NAMES[name] for name in parameter_names] + output = self.ask(f"SNAP? {','.join(p_ids)}") + return tuple(float(val) for val in output.split(",")) + + def get_data_channels_values(self) -> tuple[float, ...]: + """ + Queries the current values of the data channels + + Returns: + tuple of 4 values of the data channels + + """ + output = self.ask("SNAPD?") + return tuple(float(val) for val in output.split(",")) + + def get_data_channels_parameters( + self, query_instrument: bool = True + ) -> tuple[str, ...]: + """ + Convenience method to query a list of parameters which the data + channels are currently assigned to. + + Args: + query_instrument: If set to False, the internally cashed names of + the parameters will be returned; if True, then the names will + be queried through the instrument + + Returns: + a tuple of 4 strings of parameter names + + """ + if query_instrument: + method_name = "get" + else: + method_name = "get_latest" + + return tuple( + getattr(self.data_channels[i].assigned_parameter, method_name)() + for i in range(self._N_DATA_CHANNELS) + ) + + def get_data_channels_dict(self, requery_names: bool = False) -> dict[str, float]: + """ + Returns a dictionary where the keys are parameter names currently + assigned to the data channels, and values are the values of those + parameters. + + Args: + requery_names: if False, the currently assigned parameter names + will not be queries from the instrument in order to save time + on communication, in this case the cached assigned parameter + names will be used for the keys of the dicitonary; if True, + the assigned parameter names will be queried from the + instrument + + Returns: + a dictionary where keys are names of parameters assigned to the + data channels, and values are the values of those parameters + + """ + parameter_names = self.get_data_channels_parameters(requery_names) + parameter_values = self.get_data_channels_values() + return dict(zip(parameter_names, parameter_values)) diff --git a/src/qcodes/utils/__init__.py b/src/qcodes/utils/__init__.py index 62444140d418..0f8e4c27a235 100644 --- a/src/qcodes/utils/__init__.py +++ b/src/qcodes/utils/__init__.py @@ -1,3 +1,4 @@ +from ._full_class import full_class from .abstractmethod import qcodes_abstractmethod from .attribute_helpers import ( DelegateAttributes, @@ -9,8 +10,7 @@ ) from .deep_update_utils import deep_update from .delaykeyboardinterrupt import DelayedKeyboardInterrupt -from .deprecate import QCoDeSDeprecationWarning -from .full_class import full_class +from .deprecate import QCoDeSDeprecationWarning, _keep_attributes_shadowing_submodules from .function_helpers import is_function from .installation_info import ( convert_legacy_version_to_supported_version, @@ -24,6 +24,11 @@ from .snapshot_helpers import ParameterDiff, diff_param_values, extract_param_values from .threading_utils import RespondingThread, thread_map +# Keep the public objects when the deprecated module aliases that share +# their names are imported. +_keep_attributes_shadowing_submodules(__name__) + + __all__ = [ "DelayedKeyboardInterrupt", "DelegateAttributes", diff --git a/src/qcodes/utils/_full_class.py b/src/qcodes/utils/_full_class.py new file mode 100644 index 000000000000..b2ec19c925c8 --- /dev/null +++ b/src/qcodes/utils/_full_class.py @@ -0,0 +1,3 @@ +def full_class(obj: object) -> str: + """The full importable path to an object's class.""" + return type(obj).__module__ + "." + type(obj).__name__ diff --git a/src/qcodes/utils/deprecate.py b/src/qcodes/utils/deprecate.py index 76b9c3ef7ed0..3d48cd4e703f 100644 --- a/src/qcodes/utils/deprecate.py +++ b/src/qcodes/utils/deprecate.py @@ -1,6 +1,8 @@ from __future__ import annotations +import sys import warnings +from types import ModuleType from typing import TYPE_CHECKING, Any if TYPE_CHECKING: @@ -14,6 +16,61 @@ class QCoDeSDeprecationWarning(RuntimeWarning): """ +def _warn_deprecated_module(module_name: str, replacement: str) -> None: + """Warn that the module ``module_name`` is a deprecated alias. + + Call this from the body of the deprecated module. + + Args: + module_name: The ``__name__`` of the deprecated module. + replacement: The module that the public names should be imported from. + + """ + warnings.warn( + f"The module {module_name!r} is deprecated and will be removed in a " + f"future release. Import from {replacement!r} instead.", + QCoDeSDeprecationWarning, + stacklevel=2, + ) + + +class _KeepAttributesShadowingSubmodulesModule(ModuleType): + """A module whose public attributes are not replaced by same named submodules. + + When a submodule is imported, the import system sets it as an attribute of + its parent package. If the package exports an object with the same name as + a (deprecated) submodule, e.g. the ``SR830`` class and the + ``stanford_research.SR830`` module, importing the submodule would otherwise + replace the exported object with the submodule. + """ + + def __setattr__(self, name: str, value: object) -> None: + if ( + isinstance(value, ModuleType) + and value.__name__ == f"{self.__name__}.{name}" + and name in self.__dict__ + and not isinstance(self.__dict__[name], ModuleType) + ): + return + super().__setattr__(name, value) + + +def _keep_attributes_shadowing_submodules(module_name: str) -> None: + """ + Prevent the import of a submodule from replacing an attribute of the same name + on the module ``module_name``. + + Use this in a package that exports objects with the same names as deprecated + module aliases. Call it from the package ``__init__`` before importing the + exported objects. + + Args: + module_name: The ``__name__`` of the package. + + """ + sys.modules[module_name].__class__ = _KeepAttributesShadowingSubmodulesModule + + def _make_deprecated_typevars_getattr( module_name: str, deprecated: dict[str, Any], diff --git a/src/qcodes/utils/full_class.py b/src/qcodes/utils/full_class.py index b2ec19c925c8..fb7df62505c8 100644 --- a/src/qcodes/utils/full_class.py +++ b/src/qcodes/utils/full_class.py @@ -1,3 +1,6 @@ -def full_class(obj: object) -> str: - """The full importable path to an object's class.""" - return type(obj).__module__ + "." + type(obj).__name__ +from qcodes.utils._full_class import full_class +from qcodes.utils.deprecate import _warn_deprecated_module + +_warn_deprecated_module(__name__, "qcodes.utils") + +__all__ = ["full_class"] diff --git a/tests/dataset/dond/test_do1d.py b/tests/dataset/dond/test_do1d.py index fc6072af1567..4acf9ecda46e 100644 --- a/tests/dataset/dond/test_do1d.py +++ b/tests/dataset/dond/test_do1d.py @@ -374,7 +374,7 @@ def break_condition() -> bool: assert isinstance(data[0], DataSet) is True assert ( - "qcodes.dataset.dond.do_nd_utils", + "qcodes.dataset._dond.do_nd_utils", logging.WARNING, "Measurement has been interrupted, data may be incomplete: Break condition was met.", ) in caplog.record_tuples diff --git a/tests/dataset/dond/test_do2d_retrace.py b/tests/dataset/dond/test_do2d_retrace.py new file mode 100644 index 000000000000..dc7a9dd00365 --- /dev/null +++ b/tests/dataset/dond/test_do2d_retrace.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import numpy as np +import pytest + +from qcodes.dataset import do2d_retrace +from qcodes.parameters import Parameter + + +@pytest.mark.usefixtures("experiment") +def test_do2d_retrace( + _param_set: Parameter[float | None], _param_set_2: Parameter[float | None] +) -> None: + def measure() -> float: + outer = _param_set.cache.get() + inner = _param_set_2.cache.get() + assert outer is not None + assert inner is not None + return 10 * outer + inner + + meas_param = Parameter("meas_param", set_cmd=None, get_cmd=measure) + calls: list[str] = [] + + trace, retrace = do2d_retrace( + _param_set, + 0, + 1, + 2, + 0, + _param_set_2, + 0, + 2, + 3, + 0, + meas_param, + enter_actions=(lambda: calls.append("enter"),), + exit_actions=(lambda: calls.append("exit"),), + before_inner_actions=(lambda: calls.append("before_inner"),), + after_inner_actions=(lambda: calls.append("after_inner"),), + measurement_name="my_measurement", + show_progress=False, + ) + + assert trace.name == "my_measurement" + assert retrace.name == "retrace my_measurement" + + outer_setpoints = np.array([0, 0, 0, 1, 1, 1]) + expected_inner = { + "trace": np.array([0, 1, 2, 0, 1, 2]), + "retrace": np.array([2, 1, 0, 2, 1, 0]), + } + for kind, dataset in (("trace", trace), ("retrace", retrace)): + data = dataset.get_parameter_data()["meas_param"] + np.testing.assert_array_equal(data[_param_set.full_name], outer_setpoints) + np.testing.assert_array_equal( + data[_param_set_2.full_name], expected_inner[kind] + ) + np.testing.assert_array_equal( + data["meas_param"], 10 * outer_setpoints + expected_inner[kind] + ) + + # after_inner_actions are post actions of the inner sweeps so they run + # after each point of the inner sweeps + inner_sweep = ["before_inner"] + ["after_inner"] * 3 + assert calls == ["enter"] + inner_sweep * 2 * 2 + ["exit"] + + +@pytest.mark.usefixtures("experiment") +def test_do2d_retrace_additional_setpoints( + _param: Parameter[int], + _param_set: Parameter[float | None], + _param_set_2: Parameter[float | None], +) -> None: + additional_setpoint = Parameter( + "additional_setpoint", set_cmd=None, initial_value=5 + ) + + trace, retrace = do2d_retrace( + _param_set, + 0, + 1, + 2, + 0, + _param_set_2, + 0, + 1, + 2, + 0, + _param, + additional_setpoints=(additional_setpoint,), + show_progress=False, + ) + + for dataset in (trace, retrace): + data = dataset.get_parameter_data()[_param.full_name] + assert set(data) == { + _param.full_name, + _param_set.full_name, + _param_set_2.full_name, + additional_setpoint.full_name, + } + np.testing.assert_array_equal(data[additional_setpoint.full_name], [5] * 4) + np.testing.assert_array_equal(data[_param.full_name], [1] * 4) diff --git a/tests/dataset/dond/test_dond.py b/tests/dataset/dond/test_dond.py index dc12841c423d..be17d970d4c9 100644 --- a/tests/dataset/dond/test_dond.py +++ b/tests/dataset/dond/test_dond.py @@ -29,8 +29,8 @@ dond, new_experiment, ) +from qcodes.dataset._dond.do_nd import _Sweeper from qcodes.dataset.data_set import DataSet -from qcodes.dataset.dond.do_nd import _Sweeper from qcodes.instrument_drivers.mock_instruments import ( ArraySetPointParam, DummyChannelInstrument, diff --git a/tests/dataset/dond/test_dond_keyboard_interrupts.py b/tests/dataset/dond/test_dond_keyboard_interrupts.py index 91345a134c2f..46eb6838c990 100644 --- a/tests/dataset/dond/test_dond_keyboard_interrupts.py +++ b/tests/dataset/dond/test_dond_keyboard_interrupts.py @@ -2,7 +2,7 @@ import pytest -from qcodes.dataset.dond.do_nd_utils import BreakConditionInterrupt, catch_interrupts +from qcodes.dataset._dond.do_nd_utils import BreakConditionInterrupt, catch_interrupts if TYPE_CHECKING: from collections.abc import Iterator diff --git a/tests/drivers/test_sr830.py b/tests/drivers/test_sr830.py index 31831a60c25a..ef8c97b4a89f 100644 --- a/tests/drivers/test_sr830.py +++ b/tests/drivers/test_sr830.py @@ -11,9 +11,9 @@ import pytest from qcodes.instrument_drivers.mock_instruments import DummyInstrument -from qcodes.instrument_drivers.stanford_research.SR830 import ( - ChannelBuffer, - ChannelTrace, +from qcodes.instrument_drivers.stanford_research._sr830 import ( + SR830ChannelBuffer, + SR830ChannelTrace, ) from qcodes.validators import Arrays @@ -34,10 +34,10 @@ def test_channel_trace_requires_sr830(not_an_sr830: DummyInstrument) -> None: with pytest.raises( ValueError, match=re.escape( - "Invalid parent instrument. ChannelBuffer can only live on an SR830." + "Invalid parent instrument. SR830ChannelTrace can only live on an SR830." ), ): - ChannelTrace( + SR830ChannelTrace( "ch1_datatrace", channel=1, instrument=not_an_sr830, @@ -49,10 +49,10 @@ def test_channel_buffer_requires_sr830(not_an_sr830: DummyInstrument) -> None: with pytest.raises( ValueError, match=re.escape( - "Invalid parent instrument. ChannelBuffer can only live on an SR830." + "Invalid parent instrument. SR830ChannelBuffer can only live on an SR830." ), ): - ChannelBuffer( + SR830ChannelBuffer( "ch1_databuffer", instrument=not_an_sr830, # type: ignore[arg-type] channel=1, @@ -65,14 +65,14 @@ def test_buffer_parameters_reject_invalid_channel( ) -> None: match = re.escape("Invalid channel specifier. SR830 only has channels 1 and 2.") with pytest.raises(ValueError, match=match): - ChannelTrace( + SR830ChannelTrace( f"ch{channel}_datatrace", channel=channel, instrument=not_an_sr830, vals=Arrays(shape=(1,)), ) with pytest.raises(ValueError, match=match): - ChannelBuffer( + SR830ChannelBuffer( f"ch{channel}_databuffer", instrument=not_an_sr830, # type: ignore[arg-type] channel=channel, diff --git a/tests/drivers/test_stanford_research_sr560.py b/tests/drivers/test_stanford_research_sr560.py new file mode 100644 index 000000000000..436ead3927e0 --- /dev/null +++ b/tests/drivers/test_stanford_research_sr560.py @@ -0,0 +1,68 @@ +from typing import TYPE_CHECKING + +import pytest + +from qcodes.instrument_drivers.stanford_research import SR560, SR560VoltageParameter +from qcodes.parameters import Parameter + +if TYPE_CHECKING: + from collections.abc import Generator + + +@pytest.fixture(name="sr560") +def _make_sr560() -> "Generator[SR560, None, None]": + driver = SR560("sr560") + try: + yield driver + finally: + driver.close() + + +def test_initial_values(sr560: SR560) -> None: + assert sr560.cutoff_lo() == "DC" + assert sr560.cutoff_hi() == 1e6 + assert sr560.invert() is True + assert sr560.gain() == 10 + assert sr560.get_idn() == { + "vendor": "Stanford Research Systems", + "model": "SR560", + "serial": None, + "firmware": None, + } + + +@pytest.mark.parametrize( + ("name", "valid", "invalid"), + [ + ("cutoff_lo", 0.03, 0.05), + ("cutoff_hi", "DC", 2e6), + ("gain", 50000, 3), + ], +) +def test_parameters_validate( + sr560: SR560, name: str, valid: float | str, invalid: float +) -> None: + param = sr560.parameters[name] + assert isinstance(param, Parameter) + + param(valid) + assert param() == valid + + with pytest.raises(ValueError): + param(invalid) + + +@pytest.mark.parametrize(("invert", "sign"), [(True, -1), (False, 1)]) +def test_voltage_parameter(sr560: SR560, invert: bool, sign: int) -> None: + measured = Parameter( + "measured", set_cmd=None, initial_value=2.0, unit="V", label="Measured" + ) + sr560.gain(100) + sr560.invert(invert) + + volt = SR560VoltageParameter(measured, sr560) + + assert volt.names == ("measured_raw", "volt") + assert volt.units == ("V", "V") + assert volt.labels == ("Measured", "Voltage") + assert volt() == pytest.approx((2.0, sign * 2.0 / 100)) diff --git a/tests/test_import.py b/tests/test_import.py index 73d312b0720f..904297130f11 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -14,7 +14,14 @@ from __future__ import annotations +import importlib +import os +import subprocess +import sys +import textwrap import warnings +from pathlib import Path +from typing import TYPE_CHECKING import pytest @@ -27,6 +34,139 @@ import qcodes.validators from qcodes.utils import QCoDeSDeprecationWarning +if TYPE_CHECKING: + from pytest import MonkeyPatch + + +@pytest.mark.skipif( + not hasattr(sys, "set_lazy_imports"), reason="requires Python 3.15 lazy imports" +) +def test_public_reexports_with_lazy_qcodes_imports() -> None: + lazy_imports_path = Path(__file__).parents[1] / ".github" / "lazy-imports" + code = textwrap.dedent( + """ + import sys + import warnings + + warnings.simplefilter("ignore") + + import qcodes.dataset as dataset + import qcodes.utils as utils + from qcodes.dataset._dond.do_nd import dond + from qcodes.dataset.dond.do_nd import dond as deprecated_dond + from qcodes.instrument_drivers import basel, stanford_research + from qcodes.instrument_drivers.basel.BaselSP983 import BaselSP983 + from qcodes.instrument_drivers.stanford_research.SR830 import SR830 + from qcodes.utils._full_class import full_class + from qcodes.utils.full_class import full_class as deprecated_full_class + + assert sys.get_lazy_imports_filter() is not None + assert "qcodes.instrument_drivers.stanford_research.SR830" not in sys.modules + + # reify the lazy imports of the deprecated module aliases + assert deprecated_dond is dond + assert deprecated_full_class is full_class + assert isinstance(SR830, type) + assert isinstance(BaselSP983, type) + + assert dataset.dond is dond + assert utils.full_class is full_class + assert stanford_research.SR830 is SR830 + assert basel.BaselSP983 is BaselSP983 + """ + ) + + subprocess.run( + [sys.executable, "-c", code], + check=True, + env={ + **os.environ, + "PYTHON_LAZY_IMPORTS": "all", + "PYTHONPATH": os.pathsep.join( + (str(lazy_imports_path), os.environ.get("PYTHONPATH", "")) + ), + }, + ) + + +_DATASET = "qcodes.dataset" +_BASEL = "qcodes.instrument_drivers.basel" +_SRS = "qcodes.instrument_drivers.stanford_research" + + +@pytest.mark.parametrize( + ("module_name", "parent_module_name", "attribute_name"), + [ + ("qcodes.dataset.dond", _DATASET, "dond"), + ("qcodes.dataset.dond.do2d_retrace", _DATASET, "do2d_retrace"), + ("qcodes.dataset.dond.do_0d", _DATASET, "do0d"), + ("qcodes.dataset.dond.do_1d", _DATASET, "do1d"), + ("qcodes.dataset.dond.do_2d", _DATASET, "do2d"), + ("qcodes.dataset.dond.do_nd", _DATASET, "dond"), + ("qcodes.dataset.dond.do_nd_utils", _DATASET, "BreakConditionInterrupt"), + ("qcodes.dataset.dond.sweeps", _DATASET, "LinSweep"), + (f"{_BASEL}.BaselSP983", _BASEL, "BaselSP983"), + (f"{_BASEL}.BaselSP983a", _BASEL, "BaselSP983a"), + (f"{_BASEL}.BaselSP983c", _BASEL, "BaselSP983c"), + (f"{_SRS}.SG384", _SRS, "SG384"), + (f"{_SRS}.SR560", _SRS, "SR560"), + (f"{_SRS}.SR830", _SRS, "SR830"), + (f"{_SRS}.SR860", _SRS, "SR860"), + (f"{_SRS}.SR865", _SRS, "SR865"), + (f"{_SRS}.SR865A", _SRS, "SR865A"), + (f"{_SRS}.SR86x", _SRS, "SR86x"), + ("qcodes.utils.full_class", "qcodes.utils", "full_class"), + ], +) +def test_deprecated_module_aliases( + module_name: str, + parent_module_name: str, + attribute_name: str, + monkeypatch: MonkeyPatch, +) -> None: + parent_module = importlib.import_module(parent_module_name) + expected = getattr(parent_module, attribute_name) + # Import the alias (and for the dond subpackage, the package that warns) + # afresh so that the deprecation warning is emitted in this test. + for name in list(sys.modules): + if name == module_name or name.startswith("qcodes.dataset.dond"): + monkeypatch.delitem(sys.modules, name) + + with pytest.warns( + QCoDeSDeprecationWarning, match=r"The module .* is deprecated" + ) as warning_records: + deprecated_module = importlib.import_module(module_name) + + assert len(warning_records) == 1 + if module_name != "qcodes.dataset.dond": + assert getattr(deprecated_module, attribute_name) is expected + for name in deprecated_module.__all__: + assert getattr(deprecated_module, name) is not None + # importing a module sets it as an attribute on its parent package. + # Make sure that this does not replace the public object of the same name. + assert getattr(parent_module, attribute_name) is expected + + +@pytest.mark.parametrize( + ("module_name", "old_name", "new_module_name", "new_name"), + [ + ("SR560", "VoltageParameter", "_sr560", "SR560VoltageParameter"), + ("SR830", "ChannelBuffer", "_sr830", "SR830ChannelBuffer"), + ("SR830", "ChannelTrace", "_sr830", "SR830ChannelTrace"), + ("SR830", "GeneratedSetPoints", "_sr830", "SR830GeneratedSetPoints"), + ], +) +def test_deprecated_module_alias_renamed_classes( + module_name: str, old_name: str, new_module_name: str, new_name: str +) -> None: + new_module = importlib.import_module(f"{_SRS}.{new_module_name}") + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", QCoDeSDeprecationWarning) + deprecated_module = importlib.import_module(f"{_SRS}.{module_name}") + + assert getattr(deprecated_module, old_name) is getattr(new_module, new_name) + def test_top_level_parameter_shorthand_deprecated() -> None: with pytest.warns(