diff --git a/docs/changes/newsfragments/6832.improved b/docs/changes/newsfragments/6832.improved new file mode 100644 index 000000000000..afe6f5d15678 --- /dev/null +++ b/docs/changes/newsfragments/6832.improved @@ -0,0 +1,4 @@ +``DelegateParameter`` now includes validators of its source Parameter into its validators. This ensures that a ``DelegateParameter`` +with a non numeric source parameter is registered correctly in a measurement when the ``DelegateParameter`` it self does not +set a validator. Furthermore `DelegateParameter`` has gained ``root_source`` and ``root_delegate`` attributes that makes it easier to get the +root source or ``DelegateParameter`` of a ``DelegateParameter`` that delegates to another ``DelegateParameter`` in a chain. diff --git a/src/qcodes/parameters/delegate_parameter.py b/src/qcodes/parameters/delegate_parameter.py index b4c6ecfedf68..f1647ca7ea87 100644 --- a/src/qcodes/parameters/delegate_parameter.py +++ b/src/qcodes/parameters/delegate_parameter.py @@ -8,6 +8,8 @@ from collections.abc import Sequence from datetime import datetime + from qcodes.validators.validators import Validator + from .parameter_base import ParamDataType, ParamRawDataType @@ -200,6 +202,40 @@ def source(self) -> Parameter | None: def source(self, source: Parameter | None) -> None: self._source: Parameter | None = source + @property + def root_source(self) -> Parameter | None: + """ + The root source parameter that this :class:`DelegateParameter` is bound to + or ``None`` if this :class:`DelegateParameter` is unbound. If + the source is it self a DelegateParameter it will recursively return that Parameter's + source until a non DelegateParameter is found. For a non DelegateParameter source + this behaves the same as ``self.source`` + + :getter: Returns the current source. + :setter: Sets the source of the first parameter in the tree that has a non-DelegateParameter source. + """ + if isinstance(self.source, DelegateParameter): + return self.source.root_source + else: + return self.source + + @root_source.setter + def root_source(self, source: Parameter | None) -> None: + self.root_delegate.source = source + + @property + def root_delegate(self) -> DelegateParameter: + """ + If this parameter is part of a chain of DelegateParameters return + the first Parameter in the chain that has a non DelegateParameter source + else return self. + """ + + if not isinstance(self.source, DelegateParameter): + return self + else: + return self.source.root_delegate + @property def snapshot_value(self) -> bool: if self.source is None: @@ -314,3 +350,18 @@ def validate(self, value: ParamDataType) -> None: super().validate(value) if self.source is not None: self.source.validate(self._from_value_to_raw_value(value)) + + @property + def validators(self) -> tuple[Validator, ...]: + """ + Tuple of all validators associated with the parameter. Note that this + includes validators of the source parameter if source parameter is set + and has any validators. + + :getter: All validators associated with the parameter. + """ + source_validators: tuple[Validator, ...] = ( + self.source.validators if self.source is not None else () + ) + + return tuple(self._vals) + source_validators diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index 452896f6fb50..6ce74a533218 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -383,9 +383,10 @@ def vals(self) -> Validator | None: RuntimeError: If removing the first validator when more than one validator is set. """ + validators = self.validators - if len(self._vals): - return self._vals[0] + if len(validators): + return validators[0] else: return None diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index 0a0e0dee2118..a7ef3ec29fdc 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -24,8 +24,14 @@ from qcodes.dataset.export_config import DataExportType from qcodes.dataset.measurements import Measurement from qcodes.dataset.sqlite.connection import atomic_transaction -from qcodes.parameters import ManualParameter, Parameter, expand_setpoints_helper +from qcodes.parameters import ( + DelegateParameter, + ManualParameter, + Parameter, + expand_setpoints_helper, +) from qcodes.station import Station +from qcodes.validators import ComplexNumbers from tests.common import retry_until_does_not_throw @@ -201,6 +207,43 @@ def test_register_custom_parameter(DAC) -> None: ) +def test_register_delegate_parameters(): + x_param = Parameter("x", set_cmd=None, get_cmd=None) + + complex_param = Parameter( + "complex_param", get_cmd=None, set_cmd=None, vals=ComplexNumbers() + ) + delegate_param = DelegateParameter("delegate", source=complex_param) + + meas = Measurement() + + meas.register_parameter(x_param) + meas.register_parameter(delegate_param, setpoints=(x_param,)) + assert len(meas.parameters) == 2 + assert meas.parameters["delegate"].type == "complex" + assert meas.parameters["x"].type == "numeric" + + +def test_register_delegate_parameters_with_late_source(): + x_param = Parameter("x", set_cmd=None, get_cmd=None) + + complex_param = Parameter( + "complex_param", get_cmd=None, set_cmd=None, vals=ComplexNumbers() + ) + delegate_param = DelegateParameter("delegate", source=None) + + meas = Measurement() + + meas.register_parameter(x_param) + + delegate_param.source = complex_param + + meas.register_parameter(delegate_param, setpoints=(x_param,)) + assert len(meas.parameters) == 2 + assert meas.parameters["delegate"].type == "complex" + assert meas.parameters["x"].type == "numeric" + + def test_unregister_parameter(DAC, DMM) -> None: """ Test the unregistering of parameters. diff --git a/tests/parameter/test_delegate_parameter.py b/tests/parameter/test_delegate_parameter.py index 07b7d093c0f2..24ef7dc41a70 100644 --- a/tests/parameter/test_delegate_parameter.py +++ b/tests/parameter/test_delegate_parameter.py @@ -574,18 +574,23 @@ def test_value_validation() -> None: source_param = Parameter("source", set_cmd=None, get_cmd=None) delegate_param = DelegateParameter("delegate", source=source_param) + # Test case where source parameter validator is None and delegate parameter validator is + # specified. delegate_param.vals = vals.Numbers(-10, 10) source_param.vals = None delegate_param.validate(1) with pytest.raises(ValueError): delegate_param.validate(11) + # Test where delegate parameter validator is None and source parameter validator is + # specified. delegate_param.vals = None source_param.vals = vals.Numbers(-5, 5) delegate_param.validate(1) with pytest.raises(ValueError): delegate_param.validate(6) + # Test case where source parameter validator is more restricted than delegate parameter. delegate_param.vals = vals.Numbers(-10, 10) source_param.vals = vals.Numbers(-5, 5) delegate_param.validate(1) @@ -594,6 +599,66 @@ def test_value_validation() -> None: with pytest.raises(ValueError): delegate_param.validate(11) + # Test case that the order of setting validator on source and delegate parameters does not matter. + source_param.vals = vals.Numbers(-5, 5) + delegate_param.vals = vals.Numbers(-10, 10) + delegate_param.validate(1) + with pytest.raises(ValueError): + delegate_param.validate(6) + with pytest.raises(ValueError): + delegate_param.validate(11) + + # Test case where delegate parameter validator is more restricted than source parameter. + delegate_param.vals = vals.Numbers(-5, 5) + source_param.vals = vals.Numbers(-10, 10) + delegate_param.validate(1) + with pytest.raises(ValueError): + delegate_param.validate(6) + with pytest.raises(ValueError): + delegate_param.validate(11) + + # Test case that the order of setting validator on source and delegate parameters does not matter. + source_param.vals = vals.Numbers(-10, 10) + delegate_param.vals = vals.Numbers(-5, 5) + delegate_param.validate(1) + with pytest.raises(ValueError): + delegate_param.validate(6) + with pytest.raises(ValueError): + delegate_param.validate(11) + + +def test_validator_delegates_as_expected() -> None: + source_param = Parameter("source", set_cmd=None, get_cmd=None) + delegate_param = DelegateParameter("delegate", source=source_param) + some_validator = vals.Numbers(-10, 10) + source_param.vals = some_validator + delegate_param.vals = None + delegate_param.validate(1) + with pytest.raises(ValueError): + delegate_param.validate(11) + assert delegate_param.validators == (some_validator,) + assert delegate_param.vals == some_validator + + +def test_validator_delegates_and_source() -> None: + source_param = Parameter("source", set_cmd=None, get_cmd=None) + delegate_param = DelegateParameter("delegate", source=source_param) + some_validator = vals.Numbers(-10, 10) + some_other_validator = vals.Numbers(-5, 5) + source_param.vals = some_validator + delegate_param.vals = some_other_validator + delegate_param.validate(1) + with pytest.raises(ValueError): + delegate_param.validate(6) + assert delegate_param.validators == (some_other_validator, some_validator) + assert delegate_param.vals == some_other_validator + + assert delegate_param.source is not None + delegate_param.source.vals = None + + assert delegate_param.validators == (some_other_validator,) + assert delegate_param.vals == some_other_validator + def test_value_validation_with_offset_and_scale() -> None: source_param = Parameter( @@ -639,7 +704,7 @@ def test_value_validation_with_offset_and_scale() -> None: delegate_param.set(1) -def test_delegate_of_delegate_updates_settable_gettable(): +def test_delegate_of_delegate_updates_settable_gettable() -> None: gettable_settable_source_param = Parameter( "source", set_cmd=None, get_cmd=None, vals=vals.Numbers(-5, 5) ) @@ -672,6 +737,93 @@ def test_delegate_of_delegate_updates_settable_gettable(): assert not delegate_param_outer.settable +def test_delegate_of_delegate_root_source() -> None: + gettable_settable_source_param = Parameter( + "source", set_cmd=None, get_cmd=None, vals=vals.Numbers(-5, 5) + ) + + delegate_param_inner = DelegateParameter( + "delegate_inner", source=None, vals=vals.Numbers(-10, 10) + ) + delegate_param_outer = DelegateParameter( + "delegate_outer", source=None, vals=vals.Numbers(-10, 10) + ) + delegate_param_outer.source = delegate_param_inner + delegate_param_inner.source = gettable_settable_source_param + + assert delegate_param_outer.root_source == gettable_settable_source_param + assert delegate_param_outer.source is not None + assert delegate_param_outer.source.source == gettable_settable_source_param + assert delegate_param_outer.root_delegate == delegate_param_inner + + assert delegate_param_inner.root_source == gettable_settable_source_param + assert delegate_param_inner.source == gettable_settable_source_param + assert delegate_param_inner.root_delegate == delegate_param_inner + + delegate_param_outer.root_source = None + + assert delegate_param_outer.root_source is None + assert delegate_param_outer.source is not None + assert delegate_param_outer.source.source is None + assert delegate_param_outer.root_delegate == delegate_param_inner + + assert delegate_param_inner.root_source is None + assert delegate_param_inner.source is None + assert delegate_param_inner.root_delegate == delegate_param_inner + + +def test_delegate_chain_root_source() -> None: + gettable_settable_source_param = Parameter( + "source", set_cmd=None, get_cmd=None, vals=vals.Numbers(-5, 5) + ) + + delegate_param_inner = DelegateParameter( + "delegate_inner", source=None, vals=vals.Numbers(-10, 10) + ) + delegate_param_middle = DelegateParameter( + "delegate_inner", source=None, vals=vals.Numbers(-10, 10) + ) + delegate_param_outer = DelegateParameter( + "delegate_outer", source=None, vals=vals.Numbers(-10, 10) + ) + delegate_param_outer.source = delegate_param_middle + delegate_param_middle.source = delegate_param_inner + delegate_param_inner.source = gettable_settable_source_param + + assert delegate_param_outer.root_source == gettable_settable_source_param + assert delegate_param_outer.source is not None + assert delegate_param_outer.source.source == delegate_param_inner + assert isinstance(delegate_param_outer.source.source, DelegateParameter) + assert delegate_param_outer.source.source.source == gettable_settable_source_param + assert delegate_param_outer.root_delegate == delegate_param_inner + + assert delegate_param_middle.root_source == gettable_settable_source_param + assert delegate_param_middle.source == delegate_param_inner + assert delegate_param_middle.source.source == gettable_settable_source_param + assert delegate_param_middle.root_delegate == delegate_param_inner + + assert delegate_param_inner.root_source == gettable_settable_source_param + assert delegate_param_inner.source == gettable_settable_source_param + assert delegate_param_inner.root_delegate == delegate_param_inner + + delegate_param_outer.root_source = None + + assert delegate_param_outer.root_source is None + assert delegate_param_outer.source is not None + assert delegate_param_outer.source.source is not None + assert delegate_param_outer.source.source.source is None + assert delegate_param_outer.root_delegate == delegate_param_inner + + assert delegate_param_middle.root_source is None + assert delegate_param_middle.source is not None + assert delegate_param_middle.source.source is None + assert delegate_param_outer.root_delegate == delegate_param_inner + + assert delegate_param_inner.root_source is None + assert delegate_param_inner.source is None + assert delegate_param_inner.root_delegate == delegate_param_inner + + def test_delegate_parameter_context() -> None: gettable_settable_source_param = Parameter( "source", set_cmd=None, get_cmd=None, vals=vals.Numbers(-5, 5)