from __future__ import annotations
import collections.abc
import logging
import time
import warnings
from contextlib import contextmanager
from datetime import datetime
from functools import cached_property, wraps
from typing import TYPE_CHECKING, Any, ClassVar, overload
from qcodes.metadatable import Metadatable, MetadatableWithName
from qcodes.utils import DelegateAttributes, full_class, qcodes_abstractmethod
from qcodes.validators import Enum, Ints, Validator
from .cache import _Cache, _CacheProtocol
from .named_repr import named_repr
from .permissive_range import permissive_range
# for now the type the parameter may contain is not restricted at all
ParamDataType = Any
ParamRawDataType = Any
if TYPE_CHECKING:
from collections.abc import Callable, Generator, Iterable, Mapping, Sequence, Sized
from types import TracebackType
from qcodes.instrument.base import InstrumentBase
from qcodes.logger.instrument_logger import InstrumentLoggerAdapter
LOG = logging.getLogger(__name__)
class _SetParamContext:
"""
This class is returned by the ``set_to`` method of parameter
Example usage:
>>> v = dac.voltage()
>>> with dac.voltage.set_to(-1):
... # Do stuff with the DAC output set to -1 V.
...
>>> assert abs(dac.voltage() - v) <= tolerance
"""
def __init__(
self,
parameter: ParameterBase,
value: ParamDataType,
allow_changes: bool = False,
):
self._parameter: ParameterBase = parameter
self._value = value
self._allow_changes = allow_changes
self._original_value = None
self._original_settable: bool | None = None
def __enter__(self) -> None:
self._original_value = self._parameter.cache()
if self._original_value != self._value:
self._parameter.set(self._value)
if not self._allow_changes:
self._original_settable = self._parameter.settable
self._parameter._settable = False
def __exit__(
self,
typ: type[BaseException] | None,
value: BaseException | None,
traceback: TracebackType | None,
) -> None:
if not self._allow_changes:
assert self._original_settable is not None
self._parameter._settable = self._original_settable
if self._parameter.cache() != self._original_value:
try:
self._parameter.set(self._original_value)
except Exception:
# Likely an uninitialized Parameter
LOG.info(
"Encountered an exception setting the original value "
"when exiting set_to context of "
f"{self._parameter.full_name}",
exc_info=True,
)
[docs]
def invert_val_mapping(val_mapping: Mapping[Any, Any]) -> dict[Any, Any]:
"""Inverts the value mapping dictionary for allowed parameter values"""
return {v: k for k, v in val_mapping.items()}
[docs]
class ParameterBase(MetadatableWithName):
"""
Shared behavior for all parameters. Not intended to be used
directly, normally you should use ``Parameter``, ``ArrayParameter``,
``MultiParameter``, or ``CombinedParameter``.
Note that ``CombinedParameter`` is not yet a subclass of ``ParameterBase``
Args:
name: the local name of the parameter. Must be a valid
identifier, ie no spaces or special characters or starting with a
number. If this parameter is part of an Instrument or Station,
this should match how it will be referenced from that parent,
ie ``instrument.name`` or ``instrument.parameters[name]``
instrument: the instrument this parameter
belongs to, if any
snapshot_get: False prevents any update to the
parameter during a snapshot, even if the snapshot was called with
``update=True``, for example if it takes too long to update.
Default True.
snapshot_value: False prevents parameter value to be
stored in the snapshot. Useful if the value is large.
snapshot_exclude: 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
step: max increment of parameter value.
Larger changes are broken into multiple steps this size.
When combined with delays, this acts as a ramp.
scale: 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: Compensate for a parameter specific offset. (just as scale)
get value = raw value - offset.
set value = argument + offset.
If offset and scale are used in combination, when getting a value,
first an offset is added, then the scale is applied.
inter_delay: 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: 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: A bidirectional map data/readable values to instrument
codes, expressed as a dict: ``{data_val: instrument_code}``
For example, if the instrument uses '0' to mean 1V and '1' to mean
10V, set val_mapping={1: '0', 10: '1'} and on the user side you
only see 1 and 10, never the coded '0' and '1'
If vals is omitted, will also construct a matching Enum validator.
NOTE: only applies to get if get_cmd is a string, and to set if
set_cmd is a string.
You can use ``val_mapping`` with ``get_parser``, in which case
``get_parser`` acts on the return value from the instrument first,
then ``val_mapping`` is applied (in reverse).
get_parser: Function to transform the response from get to the final
output value. See also val_mapping
set_parser: Function to transform the input set value to an encoded
value sent to the instrument. See also val_mapping.
vals: a Validator object for this parameter
max_val_age: 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.
metadata: extra information to include with the
JSON snapshot of the parameter
abstract: 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. We override a parameter by
adding one with the same name and unit. An abstract parameter
can be added in a base class and overridden in a subclass.
bind_to_instrument: Should the parameter be registered as a delegate attribute
on the instrument passed via the instrument argument.
register_name: Specifies if the parameter should be registered in datasets
using a different name than the parameter's full_name
"""
def __init__(
self,
name: str,
instrument: InstrumentBase | None,
snapshot_get: bool = True,
metadata: Mapping[Any, Any] | None = None,
step: float | None = None,
scale: float | Iterable[float] | None = None,
offset: float | Iterable[float] | None = None,
inter_delay: float = 0,
post_delay: float = 0,
val_mapping: Mapping[Any, Any] | None = None,
get_parser: Callable[..., Any] | None = None,
set_parser: Callable[..., Any] | None = None,
snapshot_value: bool = True,
snapshot_exclude: bool = False,
max_val_age: float | None = None,
vals: Validator[Any] | None = None,
abstract: bool | None = False,
bind_to_instrument: bool = True,
register_name: str | None = None,
) -> None:
super().__init__(metadata)
if not str(name).isidentifier():
raise ValueError(
f"Parameter name must be a valid identifier "
f"got {name} which is not. Parameter names "
f"cannot start with a number and "
f"must not contain spaces or special characters"
)
self._short_name = str(name)
self._register_name = register_name
self._instrument = instrument
self._snapshot_get = snapshot_get
self._snapshot_value = snapshot_value
self.snapshot_exclude = snapshot_exclude
if not isinstance(vals, (Validator, type(None))):
raise TypeError("vals must be None or a Validator")
elif val_mapping is not None:
vals = Enum(*val_mapping.keys())
if vals is not None:
self._vals: list[Validator[Any]] = [vals]
else:
self._vals = []
self.step = step
self.scale = scale
self.offset = offset
self.inter_delay = inter_delay
self.post_delay = post_delay
self.val_mapping = val_mapping
if val_mapping is None:
self.inverse_val_mapping = None
else:
self.inverse_val_mapping = invert_val_mapping(val_mapping)
self.get_parser: Callable[..., Any] | None = get_parser
self.set_parser: Callable[..., Any] | None = set_parser
# ``_Cache`` stores "latest" value (and raw value) and timestamp
# when it was set or measured
self.cache: _CacheProtocol = _Cache(self, max_val_age=max_val_age)
# ``GetLatest`` is left from previous versions where it would
# implement a subset of features which ``_Cache`` has.
# It is left for now for backwards compatibility reasons and shall
# be deprecated and removed in the future versions.
self.get_latest: GetLatest
self.get_latest = GetLatest(self)
self.get: Callable[..., ParamDataType]
self._gettable = False
if self._implements_get_raw:
self.get = self._wrap_get(self.get_raw)
self._gettable = True
elif hasattr(self, "get"):
raise RuntimeError(
f"Overwriting get in a subclass of "
f"ParameterBase: "
f"{self.full_name} is not allowed."
)
self.set: Callable[..., None]
self._settable: bool = False
if self._implements_set_raw:
self.set = self._wrap_set(self.set_raw)
self._settable = True
elif hasattr(self, "set"):
raise RuntimeError(
f"Overwriting set in a subclass of "
f"ParameterBase: "
f"{self.full_name} is not allowed."
)
# subclasses should extend this list with extra attributes they
# want automatically included in the snapshot
self._meta_attrs = [
"name",
"instrument",
"step",
"scale",
"offset",
"inter_delay",
"post_delay",
"val_mapping",
"vals",
"validators",
]
# Specify time of last set operation, used when comparing to delay to
# check if additional waiting time is needed before next set
self._t_last_set = time.perf_counter()
# should we call validate when getting data. default to False
# intended to be changed in a subclass if you want the subclass
# to perform a validation on get
self._validate_on_get: bool = False
self._abstract = abstract
if instrument is not None and bind_to_instrument:
found_as_delegate = instrument.parameters.get(name, False)
# we allow properties since a pattern that has been seen in the wild
# is properties that are used to wrap parameters of the same name
# to define an interface for the instrument
is_property = isinstance(
getattr(instrument.__class__, name, None), property
)
found_as_attr = not is_property and hasattr(instrument, name)
if found_as_delegate or found_as_attr:
existing_parameter = instrument.parameters.get(name, None)
if existing_parameter is not None and not existing_parameter.abstract:
raise KeyError(
f"Duplicate parameter name {name} on instrument {instrument}"
)
if existing_parameter is None:
existing_attribute = getattr(instrument, name, None)
if isinstance(existing_attribute, ParameterBase):
raise KeyError(
f"Duplicate parameter name {name} on instrument {instrument}"
)
elif existing_attribute is not None:
warnings.warn(
f"Parameter {name} overrides an attribute of the same name on instrument {instrument} "
"This will be an error in the future.",
)
instrument.parameters[name] = self
@property
def _implements_get_raw(self) -> bool:
implements_get_raw = hasattr(self, "get_raw") and not getattr(
self.get_raw, "__qcodes_is_abstract_method__", False
)
return implements_get_raw
@property
def _implements_set_raw(self) -> bool:
implements_set_raw = hasattr(self, "set_raw") and not getattr(
self.set_raw, "__qcodes_is_abstract_method__", False
)
return implements_set_raw
def _get_logger(self) -> InstrumentLoggerAdapter | logging.Logger:
if self.root_instrument is not None:
mylogger: InstrumentLoggerAdapter | logging.Logger = (
self.root_instrument.log
)
else:
mylogger = LOG
return mylogger
def _build__doc__(self) -> str | None:
return self.__doc__
@property
def vals(self) -> Validator | None:
"""
The first validator of the parameter. None
if no validators are set for this parameter.
:getter: Returns the first validator or None if no validators.
:setter: Sets the first validator. Set to None to remove the first validator.
Raises:
RuntimeError: If removing the first validator when more than one validator is set.
"""
if len(self._vals):
return self._vals[0]
else:
return None
@vals.setter
def vals(self, vals: Validator | None) -> None:
if vals is not None and len(self._vals) > 0:
self._vals[0] = vals
elif vals is not None:
self._vals = [vals]
elif len(self._vals) == 1:
self._vals = []
elif len(self._vals) > 1:
raise RuntimeError(
"Cannot remove default validator from parameter with additional validators."
)
else:
# setting the validator to None but the parameter already doesn't have a validator
pass
self.__doc__ = self._build__doc__()
[docs]
def add_validator(self, vals: Validator) -> None:
"""Add a validator for the parameter. The parameter is validated against
all validators in reverse order of how they are added.
Args:
vals: Validator to add to the parameter.
"""
self._vals.append(vals)
self.__doc__ = self._build__doc__()
[docs]
def remove_validator(self) -> Validator | None:
"""
Remove the last validator added to the parameter and return it.
Returns None if there are no validators associated with the parameter.
Returns:
The last validator added to the parameter or None if there are no
validators associated with the parameter.
"""
if len(self._vals) > 0:
removed = self._vals.pop()
self.__doc__ = self._build__doc__()
return removed
else:
return None
@property
def validators(self) -> tuple[Validator, ...]:
"""
Tuple of all validators associated with the parameter.
:getter: All validators associated with the parameter.
"""
return tuple(self._vals)
@property
def raw_value(self) -> ParamRawDataType:
"""
Note that this property will be deprecated soon. Use
``cache.raw_value`` instead.
Represents the cached raw value of the parameter.
:getter: Returns the cached raw value of the parameter.
"""
return self.cache.raw_value
[docs]
@qcodes_abstractmethod
def get_raw(self) -> ParamRawDataType:
"""
``get_raw`` is called to perform the actual data acquisition from the
instrument. This method should either be overwritten to perform the
desired operation or alternatively for :class:`.Parameter` a
suitable method is automatically generated if ``get_cmd`` is supplied
to the parameter constructor. The method is automatically wrapped to
provide a ``get`` method on the parameter instance.
"""
raise NotImplementedError
[docs]
@qcodes_abstractmethod
def set_raw(self, value: ParamRawDataType) -> None:
"""
``set_raw`` is called to perform the actual setting of a parameter on
the instrument. This method should either be overwritten to perform the
desired operation or alternatively for :class:`.Parameter` a
suitable method is automatically generated if ``set_cmd`` is supplied
to the parameter constructor. The method is automatically wrapped to
provide a ``set`` method on the parameter instance.
"""
raise NotImplementedError
[docs]
def __str__(self) -> str:
"""Include the instrument name with the Parameter name if possible."""
inst_name = getattr(self._instrument, "name", "")
if inst_name:
return f"{inst_name}_{self.name}"
else:
return self.name
def __repr__(self) -> str:
return named_repr(self)
@overload
def __call__(self) -> ParamDataType:
pass
@overload
def __call__(self, value: ParamDataType, **kwargs: Any) -> None:
pass
def __call__(self, *args: Any, **kwargs: Any) -> ParamDataType | None:
if len(args) == 0 and len(kwargs) == 0:
if self.gettable:
return self.get()
else:
raise NotImplementedError(f"no get cmd found in Parameter {self.name}")
elif self.settable:
self.set(*args, **kwargs)
return None
else:
raise NotImplementedError(f"no set cmd found in Parameter {self.name}")
[docs]
def snapshot_base(
self,
update: bool | None = True,
params_to_skip_update: Sequence[str] | None = None,
) -> dict[Any, Any]:
"""
State of the parameter as a JSON-compatible dict (everything that
the custom JSON encoder class
:class:`.NumpyJSONEncoder` supports).
If the parameter has been initiated with ``snapshot_value=False``,
the snapshot will NOT include the ``value`` and ``raw_value`` of the
parameter.
Args:
update: If True, update the state by calling ``parameter.get()``
unless ``snapshot_get`` of the parameter is ``False``.
If ``update`` is ``None``, use the current value from the
``cache`` unless the cache is invalid. If ``False``, never call
``parameter.get()``.
params_to_skip_update: No effect but may be passed from superclass
Returns:
base snapshot
"""
if self.snapshot_exclude:
warnings.warn(
f"Parameter ({self.full_name}) is used in the snapshot while it "
f"should be excluded from the snapshot",
stacklevel=2,
)
state: dict[str, Any] = {"__class__": full_class(self), "full_name": str(self)}
if self.snapshot_value:
has_get = self.gettable
allowed_to_call_get_when_snapshotting = (
self._snapshot_get and update is not False
)
can_call_get_when_snapshotting = (
allowed_to_call_get_when_snapshotting and has_get
)
if can_call_get_when_snapshotting and update:
state["value"] = self.get()
else:
state["value"] = self.cache.get(
get_if_invalid=can_call_get_when_snapshotting
)
state["raw_value"] = self.cache.raw_value
state["ts"] = self.cache.timestamp
if isinstance(state["ts"], datetime):
dttime: datetime = state["ts"]
state["ts"] = dttime.strftime("%Y-%m-%d %H:%M:%S")
for attr in set(self._meta_attrs):
if attr == "instrument" and self._instrument:
state.update(
{
"instrument": full_class(self._instrument),
"instrument_name": self._instrument.name,
}
)
elif attr == "validators":
state["validators"] = [repr(validator) for validator in self.validators]
else:
val = getattr(self, attr, None)
if val is not None:
attr_strip = attr.lstrip("_") # strip leading underscores
if isinstance(val, Validator):
state[attr_strip] = repr(val)
elif isinstance(val, Metadatable):
state[attr_strip] = val.snapshot(update=update)
else:
state[attr_strip] = val
return state
@property
def snapshot_value(self) -> bool:
"""
If True the value of the parameter will be included in the snapshot.
"""
return self._snapshot_value
def _from_value_to_raw_value(self, value: ParamDataType) -> ParamRawDataType:
raw_value: ParamRawDataType
if self.val_mapping is not None:
# Convert set values using val_mapping dictionary
raw_value = self.val_mapping[value]
else:
raw_value = value
# transverse transformation in reverse order as compared to
# getter: apply scale first
if self.scale is not None:
if isinstance(self.scale, collections.abc.Iterable):
# Scale contains multiple elements, one for each value
raw_value = tuple(
val * scale for val, scale in zip(raw_value, self.scale)
)
else:
# Use single scale for all values
raw_value = raw_value * self.scale
# apply offset next
if self.offset is not None:
if isinstance(self.offset, collections.abc.Iterable):
# offset contains multiple elements, one for each value
raw_value = tuple(
val + offset for val, offset in zip(raw_value, self.offset)
)
else:
# Use single offset for all values
raw_value = raw_value + self.offset
# parser last
if self.set_parser is not None:
raw_value = self.set_parser(raw_value)
return raw_value
def _from_raw_value_to_value(self, raw_value: ParamRawDataType) -> ParamDataType:
value: ParamDataType
if self.get_parser is not None:
value = self.get_parser(raw_value)
else:
value = raw_value
# apply offset first (native scale)
if self.offset is not None and value is not None:
# offset values
try:
value = value - self.offset
except TypeError:
if isinstance(self.offset, collections.abc.Iterable):
# offset contains multiple elements, one for each value
value = tuple(
val - offset for val, offset in zip(value, self.offset)
)
elif isinstance(value, collections.abc.Iterable):
# Use single offset for all values
value = tuple(val - self.offset for val in value)
else:
raise
# scale second
if self.scale is not None and value is not None:
# Scale values
try:
value = value / self.scale
except TypeError:
if isinstance(self.scale, collections.abc.Iterable):
# Scale contains multiple elements, one for each value
value = tuple(val / scale for val, scale in zip(value, self.scale))
elif isinstance(value, collections.abc.Iterable):
# Use single scale for all values
value = tuple(val / self.scale for val in value)
else:
raise
if self.inverse_val_mapping is not None:
if value in self.inverse_val_mapping:
value = self.inverse_val_mapping[value]
else:
try:
value = self.inverse_val_mapping[int(value)]
except (ValueError, KeyError):
raise KeyError(f"'{value}' not in val_mapping")
return value
def _wrap_get(
self, get_function: Callable[..., ParamRawDataType]
) -> Callable[..., ParamDataType]:
@wraps(get_function)
def get_wrapper(*args: Any, **kwargs: Any) -> ParamDataType:
if not self.gettable:
raise TypeError("Trying to get a parameter that is not gettable.")
if self.abstract:
raise NotImplementedError(
f"Trying to get an abstract parameter: {self.full_name}"
)
try:
# There might be cases where a .get also has args/kwargs
raw_value = get_function(*args, **kwargs)
value = self._from_raw_value_to_value(raw_value)
if self._validate_on_get:
self.validate(value)
self.cache._update_with(value=value, raw_value=raw_value)
return value
except Exception as e:
e.args = (*e.args, f"getting {self}")
raise e
return get_wrapper
def _wrap_set(self, set_function: Callable[..., None]) -> Callable[..., None]:
@wraps(set_function)
def set_wrapper(value: ParamDataType, **kwargs: Any) -> None:
try:
if not self.settable:
raise TypeError("Trying to set a parameter that is not settable.")
if self.abstract:
raise NotImplementedError(
f"Trying to set an abstract parameter: {self.full_name}"
)
self.validate(value)
# In some cases intermediate sweep values must be used.
# Unless `self.step` is defined, get_sweep_values will return
# a list containing only `value`.
steps = self.get_ramp_values(value, step=self.step)
for step_index, val_step in enumerate(steps):
# even if the final value is valid we may be generating
# steps that are not so validate them too
self.validate(val_step)
raw_val_step = self._from_value_to_raw_value(val_step)
# Check if delay between set operations is required
t_elapsed = time.perf_counter() - self._t_last_set
if t_elapsed < self.inter_delay:
# Sleep until time since last set is larger than
# self.inter_delay
time.sleep(self.inter_delay - t_elapsed)
# Start timer to measure execution time of set_function
t0 = time.perf_counter()
set_function(raw_val_step, **kwargs)
# Update last set time (used for calculating delays)
self._t_last_set = time.perf_counter()
# Check if any delay after setting is required
t_elapsed = self._t_last_set - t0
if t_elapsed < self.post_delay:
# Sleep until total time is larger than self.post_delay
time.sleep(self.post_delay - t_elapsed)
self.cache._update_with(value=val_step, raw_value=raw_val_step)
except Exception as e:
e.args = (*e.args, f"setting {self} to {value}")
raise e
return set_wrapper
[docs]
def get_ramp_values(
self, value: float | Sized, step: float | None = None
) -> Sequence[float | Sized]:
"""
Return values to sweep from current value to target value.
This method can be overridden to have a custom sweep behaviour.
It can even be overridden by a generator.
Args:
value: target value
step: maximum step size
Returns:
List of stepped values, including target value.
"""
if step is None:
return [value]
else:
if isinstance(value, collections.abc.Sized) and len(value) > 1:
raise RuntimeError(
"Don't know how to step a parameter with more than one value"
)
if self.get_latest() is None:
self.get()
start_value = self.get_latest()
if not (
isinstance(start_value, (int, float))
and isinstance(value, (int, float))
):
# parameter is numeric but either one of the endpoints
# is not or the starting point is unknown. The later
# can happen for a non gettable parameter in the initial set
# operation.
LOG.warning(
f"cannot sweep {self.name} from {start_value!r} "
f"to {value!r} - jumping."
)
return [value]
# drop the initial value, we're already there
return permissive_range(start_value, value, step)[1:] + [value]
@cached_property
def _validate_context(self) -> str:
# return string describing the context for a validator
if self._instrument:
context = (
(
getattr(self._instrument, "name", "")
or str(self._instrument.__class__)
)
+ "."
+ self.name
)
else:
context = self.name
return "Parameter: " + context
[docs]
def validate(self, value: ParamDataType) -> None:
"""
Validate the value supplied.
Args:
value: value to validate
Raises:
TypeError: If the value is of the wrong type.
ValueError: If the value is outside the bounds specified by the
validator.
"""
for validator in reversed(self._vals):
if validator is not None:
validator.validate(value, self._validate_context)
@property
def step(self) -> float | None:
"""
Stepsize that this Parameter uses during set operation.
Stepsize must be a positive number or None.
If step is a positive number, this is the maximum value change
allowed in one hardware call, so a single set can result in many
calls to the hardware if the starting value is far from the target.
All but the final change will attempt to change by +/- step exactly.
If step is None stepping will not be used.
:getter: Returns the current stepsize.
:setter: Sets the value of the step.
Raises:
TypeError: if step is set to not numeric or None
ValueError: if step is set to negative
TypeError: if step is set to not integer or None for an
integer parameter
TypeError: if step is set to not a number on None
"""
return self._step
@step.setter
def step(self, step: float | None) -> None:
if step is None:
self._step: float | None = step
elif not all(getattr(vals, "is_numeric", True) for vals in self._vals):
raise TypeError("you can only step numeric parameters")
elif not isinstance(step, (int, float)):
raise TypeError("step must be a number")
elif step == 0:
self._step = None
elif step <= 0:
raise ValueError("step must be positive")
elif any(isinstance(vals, Ints) for vals in self._vals) and not isinstance(
step, int
):
raise TypeError("step must be a positive int for an Ints parameter")
else:
self._step = step
@property
def post_delay(self) -> float:
"""
Delay time after *start* of set operation, for each set.
The actual time will not be shorter than this, but may be longer
if the underlying set call takes longer.
Typically used in conjunction with `step` to create an effective
ramp rate, but can also be used without a `step` to enforce a delay
*after* every set. One might think of post_delay as how long a set
operation is supposed to take. For example, there might be an
instrument that needs extra time after setting a parameter although
the command for setting the parameter returns quickly.
:getter: Returns the current post_delay.
:setter: Sets the value of the post_delay.
Raises:
TypeError: If delay is not int nor float
ValueError: If delay is negative
"""
return self._post_delay
@post_delay.setter
def post_delay(self, post_delay: float) -> None:
if not isinstance(post_delay, (int, float)):
raise TypeError(f"post_delay ({post_delay}) must be a number")
if post_delay < 0:
raise ValueError(f"post_delay ({post_delay}) must not be negative")
self._post_delay = post_delay
@property
def inter_delay(self) -> float:
"""
Delay time between consecutive set operations.
The actual time will not be shorter than this, but may be longer
if the underlying set call takes longer.
Typically used in conjunction with `step` to create an effective
ramp rate, but can also be used without a `step` to enforce a delay
*between* sets.
:getter: Returns the current inter_delay.
:setter: Sets the value of the inter_delay.
Raises:
TypeError: If delay is not int nor float
ValueError: If delay is negative
"""
return self._inter_delay
@inter_delay.setter
def inter_delay(self, inter_delay: float) -> None:
if not isinstance(inter_delay, (int, float)):
raise TypeError(f"inter_delay ({inter_delay}) must be a number")
if inter_delay < 0:
raise ValueError(f"inter_delay ({inter_delay}) must not be negative")
self._inter_delay = inter_delay
@property
def name(self) -> str:
"""Name of the parameter. This is identical to :meth:`short_name`."""
return self._short_name
@property
def short_name(self) -> str:
"""Short name of the parameter. This is without the name of the
instrument or submodule that the parameter may be bound to. For
full name refer to :meth:`full_name`."""
return self._short_name
@property
def full_name(self) -> str:
"""
Name of the parameter including the name of the instrument and
submodule that the parameter may be bound to. The names are separated
by underscores, like this: ``instrument_submodule_parameter``.
"""
return "_".join(self.name_parts)
@property
def register_name(self) -> str:
"""
Name that will be used to register this parameter in a dataset
By default, this returns ``full_name`` or the value of the
``register_name`` argument if it was passed at initialization.
"""
return self._register_name or self.full_name
@property
def instrument(self) -> InstrumentBase | None:
"""
Return the first instrument that this parameter is bound to.
E.g if this is bound to a channel it will return the channel
and not the instrument that the channel is bound too. Use
:meth:`root_instrument` to get the real instrument.
"""
return self._instrument
@property
def root_instrument(self) -> InstrumentBase | None:
"""
Return the fundamental instrument that this parameter belongs too.
E.g if the parameter is bound to a channel this will return the
fundamental instrument that that channel belongs to. Use
:meth:`instrument` to get the channel.
"""
if self._instrument is not None:
return self._instrument.root_instrument
else:
return None
[docs]
def set_to(
self, value: ParamDataType, allow_changes: bool = False
) -> _SetParamContext:
"""
Use a context manager to temporarily set a parameter to a value. By
default, the parameter value cannot be changed inside the context.
This may be overridden with ``allow_changes=True``.
Examples:
>>> from qcodes.parameters import Parameter
>>> p = Parameter("p", set_cmd=None, get_cmd=None)
>>> p.set(2)
>>> with p.set_to(3):
... print(f"p value in with block {p.get()}") # prints 3
... p.set(5) # raises an exception
>>> print(f"p value outside with block {p.get()}") # prints 2
>>> with p.set_to(3, allow_changes=True):
... p.set(5) # now this works
>>> print(f"value after second block: {p.get()}") # still prints 2
"""
context_manager = _SetParamContext(self, value, allow_changes=allow_changes)
return context_manager
[docs]
def restore_at_exit(self, allow_changes: bool = True) -> _SetParamContext:
"""
Use a context manager to restore the value of a parameter after a
``with`` block.
By default, the parameter value may be changed inside the block, but
this can be prevented with ``allow_changes=False``. This can be
useful, for example, for debugging a complex measurement that
unintentionally modifies a parameter.
Example:
>>> p = Parameter("p", set_cmd=None, get_cmd=None)
>>> p.set(2)
>>> with p.restore_at_exit():
... p.set(3)
... print(f"value inside with block: {p.get()}") # prints 3
>>> print(f"value after with block: {p.get()}") # prints 2
>>> with p.restore_at_exit(allow_changes=False):
... p.set(5) # raises an exception
"""
return self.set_to(self.cache(), allow_changes=allow_changes)
@property
def name_parts(self) -> list[str]:
"""
List of the parts that make up the full name of this parameter
"""
if self.instrument is not None:
name_parts = getattr(self.instrument, "name_parts", [])
if name_parts == []:
# add fallback for the case where someone has bound
# the parameter to something that is not an instrument
# but perhaps it has a name anyway?
name = getattr(self.instrument, "name", None)
if name is not None:
name_parts = [name]
else:
name_parts = []
name_parts.append(self.short_name)
return name_parts
@property
def gettable(self) -> bool:
"""
Is it allowed to call get on this parameter?
"""
return self._gettable
@property
def settable(self) -> bool:
"""
Is it allowed to call set on this parameter?
"""
return self._settable
@property
def underlying_instrument(self) -> InstrumentBase | None:
"""
Returns an instance of the underlying hardware instrument that this
parameter communicates with, per this parameter's implementation.
This is useful in the case where a parameter does not belongs to
an instrument instance that represents a real hardware instrument
but actually uses a real hardware instrument in its implementation
(e.g. via calls to one or more parameters of that real hardware
instrument). This is also useful when a parameter does belong to
an instrument instance but that instance does not represent the
real hardware instrument that the parameter interacts with: hence
``root_instrument`` of the parameter cannot be the
``hardware_instrument``, however ``underlying_instrument`` can be
implemented to return the ``hardware_instrument``.
By default it returns the ``root_instrument`` of the parameter.
"""
return self.root_instrument
@property
def abstract(self) -> bool | None:
return self._abstract
class GetLatest(DelegateAttributes):
"""
Wrapper for a class:`.Parameter` that just returns the last set or measured
value stored in the class:`.Parameter` itself. If get has never been called
on the parameter or the time since get was called is larger than
``max_val_age``, get will be called on the parameter. If the parameter
does not implement get, set should be called (or the initial_value set)
before calling get on this wrapper. It is an error to set
``max_val_age`` for a parameter that does not have a get function.
The functionality of this class is subsumed and improved in
parameter's cache that is accessible via ``.cache`` attribute of the
:class:`.Parameter`. Use of ``parameter.cache`` is recommended over use of
``parameter.get_latest``.
Examples:
>>> # Can be called:
>>> param.get_latest()
>>> # Or used as if it were a gettable-only parameter itself:
>>> Loop(...).each(param.get_latest)
Args:
parameter: Parameter to be wrapped.
"""
def __init__(self, parameter: ParameterBase):
self.parameter = parameter
delegate_attr_objects: ClassVar[list[str]] = ["parameter"]
omit_delegate_attrs: ClassVar[list[str]] = ["set"]
def get(self) -> ParamDataType:
"""
Return latest value if time since get was less than
`max_val_age`, otherwise perform `get()` and
return result. A `get()` will also be performed if the
parameter never has been captured.
It is recommended to use ``parameter.cache.get()`` instead.
"""
return self.parameter.cache.get()
def get_timestamp(self) -> datetime | None:
"""
Return the age of the latest parameter value.
It is recommended to use ``parameter.cache.timestamp`` instead.
"""
return self.cache.timestamp
def get_raw_value(self) -> ParamRawDataType | None:
"""
Return latest raw value of the parameter.
It is recommended to use ``parameter.cache.raw_value`` instead.
"""
return self.cache._raw_value
def __call__(self) -> ParamDataType:
"""
Same as ``get()``
It is recommended to use ``parameter.cache()`` instead.
"""
return self.cache()