# TODO (alexcjohnson) update this with the real duck-typing requirements or
# create an ABC for Parameter and MultiParameter - or just remove this statement
# if everyone is happy to use these classes.
from __future__ import annotations
import logging
import os
from types import MethodType
from typing import TYPE_CHECKING, Any, Generic, Literal
from typing_extensions import TypedDict
from .command import Command
from .parameter_base import (
InstrumentTypeVar_co,
ParameterBase,
ParameterBaseKWArgs,
ParameterDataTypeVar,
ParamRawDataType,
)
from .sweep_values import SweepFixedValues
if TYPE_CHECKING:
from collections.abc import Callable, Iterable, Mapping
from typing import NotRequired
from typing_extensions import Unpack
from qcodes.logger.instrument_logger import InstrumentLoggerAdapter
from qcodes.parameters import ParamSpecBase
from qcodes.validators import Validator
log = logging.getLogger(__name__)
[docs]
class ParameterKWArgs(
TypedDict,
Generic[ParameterDataTypeVar, InstrumentTypeVar_co],
):
"""
This TypedDict defines the type of the kwargs that can be passed to
the ``Parameter`` class.
A subclass of ``Parameter`` should take
``**kwargs: Unpack[ParameterKWArgs]`` as input and forward this to
the super class to ensure that it can accept all the arguments
defined here.
"""
# Members from ParameterBaseKWArgs are redeclared here
# so that Sphinx can discover and document them.
instrument: NotRequired[InstrumentTypeVar_co]
"""
The instrument this parameter belongs to, if any.
"""
snapshot_get: NotRequired[bool]
"""
False prevents any update to the parameter during a snapshot,
even if the snapshot was called with ``update=True``.
Default True.
"""
metadata: NotRequired[Mapping[Any, Any] | None]
"""
Additional static metadata to add to this
parameter's JSON snapshot.
"""
step: NotRequired[float | None]
"""
Max increment of parameter value.
Larger changes are broken into multiple steps this size.
When combined with delays, this acts as a ramp.
"""
scale: NotRequired[float | Iterable[float] | None]
"""
Scale to multiply value with before performing set.
The internally multiplied value is stored in
``cache.raw_value``. Can account for a voltage divider.
"""
offset: NotRequired[float | Iterable[float] | None]
"""
Compensate for a parameter specific offset.
get value = raw value - offset.
set value = argument + offset.
"""
inter_delay: NotRequired[float]
"""
Minimum time (in seconds) between successive sets.
If the previous set was less than this, it will wait until the
condition is met. Can be set to 0 to go maximum speed with
no errors.
"""
post_delay: NotRequired[float]
"""
Time (in seconds) to wait after the *start* of each set,
whether part of a sweep or not. Can be set to 0 to go maximum
speed with no errors.
"""
val_mapping: NotRequired[Mapping[Any, Any] | None]
"""
A bidirectional map of data/readable values to instrument codes,
expressed as a dict: ``{data_val: instrument_code}``.
"""
get_parser: NotRequired[Callable[..., Any] | None]
"""
Function to transform the response from get to the final
output value. See also ``val_mapping``.
"""
set_parser: NotRequired[Callable[..., Any] | None]
"""
Function to transform the input set value to an encoded
value sent to the instrument. See also ``val_mapping``.
"""
snapshot_value: NotRequired[bool]
"""
False prevents parameter value to be stored in the snapshot.
Useful if the value is large. Default True.
"""
snapshot_exclude: NotRequired[bool]
"""
True prevents parameter to be included in the snapshot.
Useful if there are many of the same parameter which are
clogging up the snapshot. Default False.
"""
max_val_age: NotRequired[float | None]
"""
The max time (in seconds) to trust a saved value obtained
from ``cache.get`` (or ``get_latest``). If this parameter has not
been set or measured more recently than this, perform an
additional measurement.
"""
vals: NotRequired[Validator[Any] | None]
"""
A Validator object for this parameter.
"""
abstract: NotRequired[bool | None]
"""
Specifies if this parameter is abstract or not. Default is False.
If the parameter is 'abstract', it *must* be overridden by a
non-abstract parameter before the instrument containing this
parameter can be instantiated.
"""
bind_to_instrument: NotRequired[bool]
"""
Should the parameter be registered as a delegate attribute
on the instrument passed via the instrument argument.
"""
register_name: NotRequired[str | None]
"""
Specifies if the parameter should be registered in datasets
using a different name than the parameter's ``full_name``.
"""
on_set_callback: NotRequired[
Callable[[ParameterBase, ParameterDataTypeVar], None] | None
]
"""
Callback called when the parameter value is set.
"""
# Members specific to ParameterKWArgs
label: NotRequired[str | None]
"""
Normally used as the axis label when this parameter is graphed,
along with ``unit``.
"""
unit: NotRequired[str | None]
"""
The unit of measure. Use ``''`` for unitless.
"""
get_cmd: NotRequired[str | Callable[..., Any] | Literal[False] | None]
"""
A command to issue to the instrument to retrieve the value of this
parameter. Can be a callable with zero args, a VISA command string,
``None`` to use ``get_raw``, or ``False`` to disable getting.
"""
set_cmd: NotRequired[str | Callable[..., Any] | Literal[False] | None]
"""
A command to issue to the instrument to set the value of this
parameter. Can be a callable with one arg, a VISA command string,
``None`` to use ``set_raw``, or ``False`` to disable setting.
Default ``False``.
"""
initial_value: NotRequired[ParameterDataTypeVar | None]
"""
Value to set the parameter to at the end of its initialization
(this is equivalent to calling ``parameter.set(initial_value)``
after parameter initialization). Cannot be passed together with
``initial_cache_value`` argument.
"""
docstring: NotRequired[str | None]
"""
Documentation string for the ``__doc__`` field of the object.
"""
initial_cache_value: NotRequired[ParameterDataTypeVar | None]
"""
Value to set the cache of the parameter to at the end of its
initialization. Cannot be passed together with ``initial_value``
argument.
"""
[docs]
class Parameter(
ParameterBase[ParameterDataTypeVar, InstrumentTypeVar_co],
Generic[ParameterDataTypeVar, InstrumentTypeVar_co],
):
"""
A parameter represents a single degree of freedom. Most often,
this is the standard parameter for Instruments, though it can also be
used as a variable, i.e. storing/retrieving a value, or be subclassed for
more complex uses.
By default only gettable, returning its last value.
This behaviour can be modified in two ways:
1. Providing a ``get_cmd``/``set_cmd``, which can do the following:
a. callable, with zero args for get_cmd, one arg for set_cmd
b. VISA command string
c. None, in which case it retrieves its last value for ``get_cmd``,
and stores a value for ``set_cmd``
d. False, in which case trying to get/set will raise an error.
2. Creating a subclass with an explicit :meth:`get_raw`/:meth:`set_raw`
method.
This enables more advanced functionality. The :meth:`get_raw` and
:meth:`set_raw` methods are automatically wrapped to provide ``get`` and
``set``.
It is an error to do both 1 and 2. E.g supply a ``get_cmd``/``set_cmd``
and implement ``get_raw``/``set_raw``
To detect if a parameter is gettable or settable check the attributes
:py:attr:`~gettable` and :py:attr:`~settable` on the parameter.
Parameters have a ``cache`` object that stores internally the current
``value`` and ``raw_value`` of the parameter. Calling ``cache.get()``
(or ``cache()``) simply returns the most recent set or measured value of
the parameter.
Parameter also has a ``.get_latest`` method that duplicates the behavior
of ``cache()`` call, as in, it also simply returns the most recent set
or measured value.
Args:
name: The local name of the parameter. Should be a valid
identifier, ie no spaces or special characters. If this parameter
is part of an Instrument or Station, this is how it will be
referenced from that parent, ie ``instrument.name`` or
``instrument.parameters[name]``.
label: Normally used as the axis label when this
parameter is graphed, along with ``unit``.
unit: The unit of measure. Use ``''`` for unitless.
get_cmd: A command to issue to the instrument to retrieve the
value of this parameter. Can be a callable with zero args,
a VISA command string, ``None`` to use ``get_raw``, or
``False`` to disable getting.
set_cmd: A command to issue to the instrument to set the
value of this parameter. Can be a callable with one arg,
a VISA command string, ``None`` to use ``set_raw``, or
``False`` to disable setting. Default ``False``.
initial_value: Value to set the parameter to at the end of its
initialization (this is equivalent to calling
``parameter.set(initial_value)`` after parameter initialization).
Cannot be passed together with ``initial_cache_value`` argument.
docstring: Documentation string for the ``__doc__``
field of the object. The ``__doc__`` field of the instance is
used by some help systems, but not all.
initial_cache_value: Value to set the cache of the parameter to
at the end of its initialization (this is equivalent to calling
``parameter.cache.set(initial_cache_value)`` after parameter
initialization). Cannot be passed together with ``initial_value``
argument.
**kwargs: Forwarded to the ``ParameterBase`` base class.
See :class:`ParameterBaseKWArgs` for details.
"""
def __init__(
self,
name: str,
*,
label: str | None = None,
unit: str | None = None,
get_cmd: str | Callable[..., Any] | Literal[False] | None = None,
set_cmd: str | Callable[..., Any] | Literal[False] | None = False,
initial_value: ParameterDataTypeVar | None = None,
docstring: str | None = None,
initial_cache_value: ParameterDataTypeVar | None = None,
**kwargs: Unpack[
ParameterBaseKWArgs[ParameterDataTypeVar, InstrumentTypeVar_co]
],
) -> None:
def _get_manual_parameter(self: Parameter) -> ParamRawDataType:
if self.root_instrument is not None:
mylogger: InstrumentLoggerAdapter | logging.Logger = (
self.root_instrument.log
)
else:
mylogger = log
mylogger.debug(
"Getting raw value of parameter: %s as %s",
self.full_name,
self.cache.raw_value,
)
return self.cache.raw_value
def _set_manual_parameter(
self: Parameter, x: ParamRawDataType
) -> ParamRawDataType:
mylogger = self._get_logger()
mylogger.debug(
"Setting raw value of parameter: %s to %s", self.full_name, x
)
self.cache._set_from_raw_value(x)
return x
instrument = kwargs.get("instrument")
bind_to_instrument = kwargs.get("bind_to_instrument", True)
max_val_age = kwargs.get("max_val_age")
if instrument is not None and bind_to_instrument:
existing_parameter = instrument.parameters.get(name, None)
if existing_parameter:
# this check is redundant since its also in the baseclass
# but if we do not put it here it would be an api break
# as parameter duplication check won't be done first,
# hence for parameters that are duplicates and have
# wrong units, users will be getting ValueError where
# they used to have KeyError before.
if not existing_parameter.abstract:
raise KeyError(
f"Duplicate parameter name {name} on instrument {instrument}"
)
existing_unit = getattr(existing_parameter, "unit", None)
if existing_unit != unit:
raise ValueError(
f"The unit of the parameter '{name}' is '{unit}'. "
f"This is inconsistent with the unit defined in the "
f"base class"
)
super().__init__(
name=name,
**kwargs,
)
no_instrument_get = not self._implements_get_raw and (
get_cmd is None or get_cmd is False
)
# TODO: a matching check should be in ParameterBase but
# due to the current limited design the ParameterBase cannot
# know if this subclass will supply a get_cmd
# To work around this a RunTime check is put into get of GetLatest
# and into get of _Cache
if max_val_age is not None and no_instrument_get:
raise SyntaxError(
"Must have get method or specify get_cmd when max_val_age is set"
)
# Enable set/get methods from get_cmd/set_cmd if given and
# no `get`/`set` or `get_raw`/`set_raw` methods have been defined
# in the scope of this class.
# (previous call to `super().__init__` wraps existing
# get_raw/set_raw into get/set methods)
if self._implements_get_raw and get_cmd not in (None, False):
raise TypeError(
"Supplying a not None or False `get_cmd` to a Parameter"
" that already implements"
" get_raw is an error."
)
elif not self._implements_get_raw and get_cmd is not False:
if get_cmd is None:
# ignore typeerror since mypy does not allow setting a method dynamically
self.get_raw = MethodType(_get_manual_parameter, self) # type: ignore[method-assign]
else:
if isinstance(get_cmd, str) and instrument is None:
raise TypeError(
f"Cannot use a str get_cmd without "
f"binding to an instrument. "
f"Got: get_cmd {get_cmd} for parameter {name}"
)
exec_str_ask = getattr(instrument, "ask", None) if instrument else None
# TODO get_raw should also be a method here. This should probably be done by wrapping
# it with MethodType like above
# ignore typeerror since mypy does not allow setting a method dynamically
self.get_raw = Command( # type: ignore[method-assign]
arg_count=0,
cmd=get_cmd,
exec_str=exec_str_ask,
)
self._gettable = True
self.get = self._wrap_get(self.get_raw)
if self._implements_set_raw and set_cmd not in (None, False):
raise TypeError(
"Supplying a not None or False `set_cmd` to a Parameter"
" that already implements"
" set_raw is an error."
)
elif not self._implements_set_raw and set_cmd is not False:
if set_cmd is None:
# ignore typeerror since mypy does not allow setting a method dynamically
self.set_raw = MethodType(_set_manual_parameter, self) # type: ignore[method-assign]
else:
if isinstance(set_cmd, str) and instrument is None:
raise TypeError(
f"Cannot use a str set_cmd without "
f"binding to an instrument. "
f"Got: set_cmd {set_cmd} for parameter {name}"
)
exec_str_write = (
getattr(instrument, "write", None) if instrument else None
)
# TODO get_raw should also be a method here. This should probably be done by wrapping
# it with MethodType like above
# ignore typeerror since mypy does not allow setting a method dynamically
self.set_raw = Command( # type: ignore[assignment]
arg_count=1, cmd=set_cmd, exec_str=exec_str_write
)
self._settable = True
self.set = self._wrap_set(self.set_raw)
self._meta_attrs.extend(["label", "unit", "vals"])
self.label = name if label is None else label
self._label: str
self.unit = unit if unit is not None else ""
self._unitval: str
if initial_value is not None and initial_cache_value is not None:
raise SyntaxError(
"It is not possible to specify both of the "
"`initial_value` and `initial_cache_value` "
"keyword arguments."
)
if initial_value is not None:
self.set(initial_value)
if initial_cache_value is not None:
self.cache.set(initial_cache_value)
self._docstring = docstring
self.__doc__ = self._build__doc__()
def _build__doc__(self) -> str:
if len(self.validators) == 0:
validator_docstrings = ["* `vals` None"]
else:
validator_docstrings = [
f"* `vals` {validator!r}" for validator in self.validators
]
# generate default docstring
doc = os.linesep.join(
(
"Parameter class:",
"",
f"* `name` {self.name}",
f"* `label` {self.label}",
f"* `unit` {self.unit}",
*validator_docstrings,
)
)
if self._docstring is not None:
doc = os.linesep.join((self._docstring, "", doc))
return doc
@property
def unit(self) -> str:
"""
The unit of measure. Use ``''`` (the empty string)
for unitless.
"""
return self._unitval
@unit.setter
def unit(self, unit: str) -> None:
self._unitval = unit
@property
def label(self) -> str:
"""
Label of the data used for plots etc.
"""
return self._label
@label.setter
def label(self, label: str) -> None:
self._label = label
[docs]
def __getitem__(self, keys: Any) -> SweepFixedValues:
"""
Slice a Parameter to get a SweepValues object
to iterate over during a sweep
"""
return SweepFixedValues(self, keys)
[docs]
def increment(self, value: ParameterDataTypeVar) -> None:
"""Increment the parameter with a value
Args:
value: Value to be added to the parameter.
"""
# this method only works with parameters that support addition
# however we don't currently enforce that via typing
self.set(self.get() + value) # type: ignore[operator]
[docs]
def sweep(
self,
start: float,
stop: float,
step: float | None = None,
num: int | None = None,
) -> SweepFixedValues:
"""
Create a collection of parameter values to be iterated over.
Requires `start` and `stop` and (`step` or `num`)
The sign of `step` is not relevant.
Args:
start: The starting value of the sequence.
stop: The end value of the sequence.
step: Spacing between values.
num: Number of values to generate.
Returns:
SweepFixedValues: Collection of parameter values to be
iterated over.
Examples:
>>> sweep(0, 10, num=5)
[0.0, 2.5, 5.0, 7.5, 10.0]
>>> sweep(5, 10, step=1)
[5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
>>> sweep(15, 10.5, step=1.5)
>[15.0, 13.5, 12.0, 10.5]
"""
return SweepFixedValues(self, start=start, stop=stop, step=step, num=num)
@property
def param_spec(self) -> ParamSpecBase:
paramspecbase = super().param_spec # Sets the name and paramtype
paramspecbase.label = self.label
paramspecbase.unit = self.unit
return paramspecbase
[docs]
class ManualParameter(Parameter):
def __init__(
self,
name: str,
**kwargs: Unpack[ParameterKWArgs],
):
"""
A simple alias for a parameter that does not have a set or
a get function. Useful for parameters that do not have a direct
instrument mapping.
Args:
name: The local name of the parameter.
**kwargs: Forwarded to the ``Parameter`` base class.
See :class:`ParameterKWArgs` for details.
Note that ``get_cmd`` and ``set_cmd`` are not allowed
since ManualParameter hardcodes these to ``None``.
Raises:
ValueError: If ``get_cmd`` or ``set_cmd`` is provided.
"""
forbidden_kwargs = ("get_cmd", "set_cmd")
for fk in forbidden_kwargs:
if fk in kwargs:
raise ValueError(
f'It is not allowed to set "{fk}" for a ManualParameter.'
)
kwargs["get_cmd"] = None
kwargs["set_cmd"] = None
super().__init__(
name=name,
**kwargs,
)