From fb558134f18c59477ca0c194d13b15db7ac1e321 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 2 May 2024 09:53:27 +0200 Subject: [PATCH 1/3] Wip experiment with model for metadata --- pyproject.toml | 1 + src/qcodes/metadatable/metadatable_base.py | 53 ++++++++++++++++++++-- src/qcodes/parameters/parameter.py | 31 ++++++++++++- src/qcodes/parameters/parameter_base.py | 25 ++++++++-- 4 files changed, 99 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f4a6d39d7d2..ccc697ff79c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ dependencies = [ "tornado>=6.3.3", "ipython>=8.10.0", "pillow>=9.0.0", + "pydantic>=2.0.0", ] dynamic = ["version"] diff --git a/src/qcodes/metadatable/metadatable_base.py b/src/qcodes/metadatable/metadatable_base.py index b07ce564b57..648a1bac3e5 100644 --- a/src/qcodes/metadatable/metadatable_base.py +++ b/src/qcodes/metadatable/metadatable_base.py @@ -1,5 +1,8 @@ from abc import abstractmethod -from typing import TYPE_CHECKING, Any, Optional, final +from typing import TYPE_CHECKING, Any, Generic, Optional, final + +from pydantic import BaseModel +from typing_extensions import TypeVar from qcodes.utils import deep_update @@ -18,9 +21,29 @@ Snapshot = dict[str, Any] -class Metadatable: - def __init__(self, metadata: Optional["Mapping[str, Any]"] = None): +class EmptyMetaDataModel(BaseModel): + pass + +MetaDataSnapShotType = TypeVar("MetaDataSnapShotType", bound=EmptyMetaDataModel) + + +class EmptyTypedSnapShot(BaseModel): + pass + + +SnapShotType = TypeVar("SnapShotType", bound=EmptyTypedSnapShot) + + +class Metadatable(Generic[SnapShotType, MetaDataSnapShotType]): + def __init__( + self, + metadata: Optional["Mapping[str, Any]"] = None, + model: type[SnapShotType] = EmptyTypedSnapShot, + metadata_model: type[MetaDataSnapShotType] = EmptyMetaDataModel, + ): self.metadata: dict[str, Any] = {} + self._model = model or EmptyTypedSnapShot + self._metadata_model = metadata_model or EmptyMetaDataModel self.load_metadata(metadata or {}) def load_metadata(self, metadata: "Mapping[str, Any]") -> None: @@ -53,6 +76,16 @@ def snapshot(self, update: Optional[bool] = False) -> Snapshot: return snap + @final + def typed_snapshot(self) -> SnapShotType: + snapshot_dict = self.snapshot() # probably want to filter metadata here + snapshot = self._model(**snapshot_dict) + return snapshot + + @final + def typed_metadata(self) -> MetaDataSnapShotType: + return self._metadata_model(**self.metadata) + def snapshot_base( self, update: Optional[bool] = False, @@ -63,8 +96,20 @@ def snapshot_base( """ return {} + # @property + # def metadata_model(self) -> type[BaseModel] | None: + # return self._metadata_model + + + # @metadata_model.setter + # def metadata_model(self, model: type[BaseModel] | None) -> None: + # self._metadata_model = model + -class MetadatableWithName(Metadatable): +class MetadatableWithName( + Metadatable[SnapShotType, MetaDataSnapShotType], + Generic[SnapShotType, MetaDataSnapShotType], +): """Add short_name and full_name properties to Metadatable. This is used as a base class for all components in QCoDeS that are members of a station to ensure that they have a name and diff --git a/src/qcodes/parameters/parameter.py b/src/qcodes/parameters/parameter.py index 422cae0055b..a2d229320c0 100644 --- a/src/qcodes/parameters/parameter.py +++ b/src/qcodes/parameters/parameter.py @@ -6,7 +6,14 @@ import logging import os from types import MethodType -from typing import TYPE_CHECKING, Any, Callable, Literal +from typing import TYPE_CHECKING, Any, Callable, Generic, Literal + +from qcodes.metadatable.metadatable_base import ( + EmptyMetaDataModel, + EmptyTypedSnapShot, + MetaDataSnapShotType, + SnapShotType, +) from .command import Command from .parameter_base import ParamDataType, ParameterBase, ParamRawDataType @@ -21,7 +28,10 @@ log = logging.getLogger(__name__) -class Parameter(ParameterBase): +class Parameter( + ParameterBase[SnapShotType, MetaDataSnapShotType], + Generic[SnapShotType, MetaDataSnapShotType], +): """ A parameter represents a single degree of freedom. Most often, this is the standard parameter for Instruments, though it can also be @@ -179,6 +189,8 @@ def __init__( docstring: str | None = None, initial_cache_value: float | str | None = None, bind_to_instrument: bool = True, + model: type[SnapShotType] = EmptyTypedSnapShot, + metadata_model: type[MetaDataSnapShotType] = EmptyMetaDataModel, **kwargs: Any, ) -> None: def _get_manual_parameter(self: Parameter) -> ParamRawDataType: @@ -240,6 +252,8 @@ def _set_manual_parameter( vals=vals, max_val_age=max_val_age, bind_to_instrument=bind_to_instrument, + model=model, + metadata_model=metadata_model, **kwargs, ) @@ -441,6 +455,19 @@ def sweep( return SweepFixedValues(self, start=start, stop=stop, step=step, num=num) +class ParameterSnapshot(EmptyTypedSnapShot): + # need to handle replacing __class__ with a different name that is compatible + value: Any # use paramdatatype + raw_value: Any # useparamdatatype + ts: str | None + inter_delay: float + name: str + post_delay: float + validators: list[str] + label: str + unit: str + + class ManualParameter(Parameter): def __init__( self, diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index d67d11f1d90..082da3d5157 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -7,9 +7,18 @@ 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 typing import TYPE_CHECKING, Any, ClassVar, Generic, overload + +from qcodes.metadatable import ( + Metadatable, + MetadatableWithName, +) +from qcodes.metadatable.metadatable_base import ( + EmptyMetaDataModel, + EmptyTypedSnapShot, + MetaDataSnapShotType, + SnapShotType, +) from qcodes.utils import DelegateAttributes, full_class, qcodes_abstractmethod from qcodes.validators import Enum, Ints, Validator @@ -85,7 +94,10 @@ def invert_val_mapping(val_mapping: Mapping[Any, Any]) -> dict[Any, Any]: return {v: k for k, v in val_mapping.items()} -class ParameterBase(MetadatableWithName): +class ParameterBase( + MetadatableWithName[SnapShotType, MetaDataSnapShotType], + Generic[SnapShotType, MetaDataSnapShotType], +): """ Shared behavior for all parameters. Not intended to be used directly, normally you should use ``Parameter``, ``ArrayParameter``, @@ -201,8 +213,11 @@ def __init__( abstract: bool | None = False, bind_to_instrument: bool = True, register_name: str | None = None, + model: type[SnapShotType] = EmptyTypedSnapShot, + metadata_model: type[MetaDataSnapShotType] = EmptyMetaDataModel, + **kwargs: Any, ) -> None: - super().__init__(metadata) + super().__init__(metadata, model=model, metadata_model=metadata_model, **kwargs) if not str(name).isidentifier(): raise ValueError( f"Parameter name must be a valid identifier " From 08c14444229abb0cb17df0bef24f3f513f6a2554 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 6 May 2024 14:34:43 +0200 Subject: [PATCH 2/3] Add tests for parameter metadata --- .../test_parameter_typed_metadata.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/parameter/test_parameter_typed_metadata.py diff --git a/tests/parameter/test_parameter_typed_metadata.py b/tests/parameter/test_parameter_typed_metadata.py new file mode 100644 index 00000000000..4a6490d1c3b --- /dev/null +++ b/tests/parameter/test_parameter_typed_metadata.py @@ -0,0 +1,43 @@ +from enum import StrEnum + +from typing_extensions import assert_type + +from qcodes.metadatable.metadatable_base import EmptyMetaDataModel +from qcodes.parameters import Parameter +from qcodes.parameters.parameter import ParameterSnapshot + + +def test_parameter_typed_metadata_basic(): + + class ParamType(StrEnum): + + voltage = "voltage" + current = "current" + + class MyParameterMetadata(EmptyMetaDataModel): + param_type: ParamType + + a = Parameter( + name="myparam", + set_cmd=None, + get_cmd=None, + model=ParameterSnapshot, + metadata_model=MyParameterMetadata, + ) + a.metadata["param_type"] = ( + ParamType.voltage + ) # TODO setting metadata should validate against the model + value = 123 + a.set(value) + + assert isinstance(a.typed_snapshot(), ParameterSnapshot) + assert isinstance(a.typed_metadata(), MyParameterMetadata) + + assert a.typed_metadata().param_type == ParamType.voltage + assert a.typed_snapshot().value == value + assert a.typed_snapshot().name == "myparam" + + assert_type( + a.typed_metadata(), MyParameterMetadata + ) # TODO this is only checked if the type checker runs agains the test + assert_type(a.typed_snapshot(), ParameterSnapshot) From 59679d405381294eadedae4ce9d04e3fa1886e0a Mon Sep 17 00:00:00 2001 From: Mind Date: Thu, 23 May 2024 21:47:15 +0200 Subject: [PATCH 3/3] Clean up names and remove redundant generic --- src/qcodes/metadatable/metadatable_base.py | 29 ++++++++++------------ src/qcodes/parameters/parameter.py | 23 ++++++++--------- src/qcodes/parameters/parameter_base.py | 21 +++++++--------- 3 files changed, 32 insertions(+), 41 deletions(-) diff --git a/src/qcodes/metadatable/metadatable_base.py b/src/qcodes/metadatable/metadatable_base.py index 648a1bac3e5..848e5668d18 100644 --- a/src/qcodes/metadatable/metadatable_base.py +++ b/src/qcodes/metadatable/metadatable_base.py @@ -21,29 +21,29 @@ Snapshot = dict[str, Any] -class EmptyMetaDataModel(BaseModel): +class EmptyMetadataModel(BaseModel): pass -MetaDataSnapShotType = TypeVar("MetaDataSnapShotType", bound=EmptyMetaDataModel) +MetadataType = TypeVar("MetadataType", bound=EmptyMetadataModel) -class EmptyTypedSnapShot(BaseModel): +class EmptySnapshotModel(BaseModel): pass -SnapShotType = TypeVar("SnapShotType", bound=EmptyTypedSnapShot) +SnapshotType = TypeVar("SnapshotType", bound=EmptySnapshotModel) -class Metadatable(Generic[SnapShotType, MetaDataSnapShotType]): +class Metadatable(Generic[SnapshotType, MetadataType]): def __init__( self, metadata: Optional["Mapping[str, Any]"] = None, - model: type[SnapShotType] = EmptyTypedSnapShot, - metadata_model: type[MetaDataSnapShotType] = EmptyMetaDataModel, + snapshot_model: type[SnapshotType] = EmptySnapshotModel, + metadata_model: type[MetadataType] = EmptyMetadataModel, ): self.metadata: dict[str, Any] = {} - self._model = model or EmptyTypedSnapShot - self._metadata_model = metadata_model or EmptyMetaDataModel + self._snapshot_model = snapshot_model or EmptySnapshotModel + self._metadata_model = metadata_model or EmptyMetadataModel self.load_metadata(metadata or {}) def load_metadata(self, metadata: "Mapping[str, Any]") -> None: @@ -77,13 +77,13 @@ def snapshot(self, update: Optional[bool] = False) -> Snapshot: return snap @final - def typed_snapshot(self) -> SnapShotType: + def typed_snapshot(self) -> SnapshotType: snapshot_dict = self.snapshot() # probably want to filter metadata here - snapshot = self._model(**snapshot_dict) + snapshot = self._snapshot_model(**snapshot_dict) return snapshot @final - def typed_metadata(self) -> MetaDataSnapShotType: + def typed_metadata(self) -> MetadataType: return self._metadata_model(**self.metadata) def snapshot_base( @@ -106,10 +106,7 @@ def snapshot_base( # self._metadata_model = model -class MetadatableWithName( - Metadatable[SnapShotType, MetaDataSnapShotType], - Generic[SnapShotType, MetaDataSnapShotType], -): +class MetadatableWithName(Metadatable[SnapshotType, MetadataType]): """Add short_name and full_name properties to Metadatable. This is used as a base class for all components in QCoDeS that are members of a station to ensure that they have a name and diff --git a/src/qcodes/parameters/parameter.py b/src/qcodes/parameters/parameter.py index a2d229320c0..4430d3554b3 100644 --- a/src/qcodes/parameters/parameter.py +++ b/src/qcodes/parameters/parameter.py @@ -6,13 +6,13 @@ import logging import os from types import MethodType -from typing import TYPE_CHECKING, Any, Callable, Generic, Literal +from typing import TYPE_CHECKING, Any, Callable, Literal from qcodes.metadatable.metadatable_base import ( - EmptyMetaDataModel, - EmptyTypedSnapShot, - MetaDataSnapShotType, - SnapShotType, + EmptyMetadataModel, + EmptySnapshotModel, + MetadataType, + SnapshotType, ) from .command import Command @@ -28,10 +28,7 @@ log = logging.getLogger(__name__) -class Parameter( - ParameterBase[SnapShotType, MetaDataSnapShotType], - Generic[SnapShotType, MetaDataSnapShotType], -): +class Parameter(ParameterBase[SnapshotType, MetadataType]): """ A parameter represents a single degree of freedom. Most often, this is the standard parameter for Instruments, though it can also be @@ -189,8 +186,8 @@ def __init__( docstring: str | None = None, initial_cache_value: float | str | None = None, bind_to_instrument: bool = True, - model: type[SnapShotType] = EmptyTypedSnapShot, - metadata_model: type[MetaDataSnapShotType] = EmptyMetaDataModel, + snapshot_model: type[SnapshotType] = EmptySnapshotModel, + metadata_model: type[MetadataType] = EmptyMetadataModel, **kwargs: Any, ) -> None: def _get_manual_parameter(self: Parameter) -> ParamRawDataType: @@ -252,7 +249,7 @@ def _set_manual_parameter( vals=vals, max_val_age=max_val_age, bind_to_instrument=bind_to_instrument, - model=model, + snapshot_model=snapshot_model, metadata_model=metadata_model, **kwargs, ) @@ -455,7 +452,7 @@ def sweep( return SweepFixedValues(self, start=start, stop=stop, step=step, num=num) -class ParameterSnapshot(EmptyTypedSnapShot): +class ParameterSnapshot(EmptySnapshotModel): # need to handle replacing __class__ with a different name that is compatible value: Any # use paramdatatype raw_value: Any # useparamdatatype diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index 082da3d5157..e133027eb6c 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -7,17 +7,17 @@ from contextlib import contextmanager from datetime import datetime from functools import cached_property, wraps -from typing import TYPE_CHECKING, Any, ClassVar, Generic, overload +from typing import TYPE_CHECKING, Any, ClassVar, overload from qcodes.metadatable import ( Metadatable, MetadatableWithName, ) from qcodes.metadatable.metadatable_base import ( - EmptyMetaDataModel, - EmptyTypedSnapShot, - MetaDataSnapShotType, - SnapShotType, + EmptyMetadataModel, + EmptySnapshotModel, + MetadataType, + SnapshotType, ) from qcodes.utils import DelegateAttributes, full_class, qcodes_abstractmethod from qcodes.validators import Enum, Ints, Validator @@ -94,10 +94,7 @@ def invert_val_mapping(val_mapping: Mapping[Any, Any]) -> dict[Any, Any]: return {v: k for k, v in val_mapping.items()} -class ParameterBase( - MetadatableWithName[SnapShotType, MetaDataSnapShotType], - Generic[SnapShotType, MetaDataSnapShotType], -): +class ParameterBase(MetadatableWithName[SnapshotType, MetadataType]): """ Shared behavior for all parameters. Not intended to be used directly, normally you should use ``Parameter``, ``ArrayParameter``, @@ -213,11 +210,11 @@ def __init__( abstract: bool | None = False, bind_to_instrument: bool = True, register_name: str | None = None, - model: type[SnapShotType] = EmptyTypedSnapShot, - metadata_model: type[MetaDataSnapShotType] = EmptyMetaDataModel, + snapshot_model: type[SnapshotType] = EmptySnapshotModel, + metadata_model: type[MetadataType] = EmptyMetadataModel, **kwargs: Any, ) -> None: - super().__init__(metadata, model=model, metadata_model=metadata_model, **kwargs) + super().__init__(metadata, snapshot_model=snapshot_model, metadata_model=metadata_model, **kwargs) if not str(name).isidentifier(): raise ValueError( f"Parameter name must be a valid identifier "