Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to lookup component in station by full name #5028

Merged
merged 17 commits into from Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/changes/newsfragments/5028.new
@@ -0,0 +1,3 @@
The QCoDeS ``Station`` ``Instrument`` and ``InstrumentModule`` classes gained a method ``get_component``
which allows the user to get a component by name. Allowing the user to go from
the full name of a component to the component itself.
67 changes: 63 additions & 4 deletions docs/examples/Station.ipynb
Expand Up @@ -18,7 +18,23 @@
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Logging hadn't been started.\n",
"Activating auto-logging. Current session state plus future input saved.\n",
"Filename : C:\\Users\\jenielse\\.qcodes\\logs\\command_history.log\n",
"Mode : append\n",
"Output logging : True\n",
"Raw input log : False\n",
"Timestamping : True\n",
"State : active\n",
"Qcodes Logfile : C:\\Users\\jenielse\\.qcodes\\logs\\230221-10976-qcodes.log\n"
]
}
],
"source": [
"# Useful imports:\n",
"\n",
Expand Down Expand Up @@ -128,7 +144,7 @@
{
"data": {
"text/plain": [
"{'p': <qcodes.instrument.parameter.Parameter: p at 2330718811616>,\n",
"{'p': <qcodes.parameters.parameter.Parameter: p at 2344293461648>,\n",
" 'instr': <DummyInstrument: instr>}"
]
},
Expand Down Expand Up @@ -183,6 +199,44 @@
"assert station.p is p"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"It is also possible to access a component or a sub component of a instrument directly via its name. For example, if we want to access the parameter ``input`` of the instrument ``instr``, we can do the following:"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"instr_input\n"
]
},
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"input_name = station.instr.input.full_name\n",
"print(input_name)\n",
"param = station.get_component(input_name)\n",
"param is instr.input"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down Expand Up @@ -674,7 +728,7 @@
"metadata": {
"file_extension": ".py",
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"display_name": "qcodespip311",
"language": "python",
"name": "python3"
},
Expand All @@ -688,13 +742,18 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.8"
"version": "3.11.0"
},
"mimetype": "text/x-python",
"name": "python",
"npconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": 3,
"vscode": {
"interpreter": {
"hash": "7ac3e91929df5618782934af11c3fa566d637713ed5d04bf73eff1f535fb8e06"
}
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
"state": {},
Expand Down
32 changes: 30 additions & 2 deletions qcodes/instrument/channel.py
Expand Up @@ -5,7 +5,7 @@
from collections.abc import Callable, Iterable, Iterator
from typing import Any, List, MutableSequence, Sequence, TypeVar, Union, cast, overload

from qcodes.metadatable import Metadatable
from qcodes.metadatable import MetadatableWithName
from qcodes.parameters import (
ArrayParameter,
MultiChannelInstrumentParameter,
Expand Down Expand Up @@ -90,7 +90,7 @@ class InstrumentChannel(InstrumentModule):
T = TypeVar("T", bound="ChannelTuple")


class ChannelTuple(Metadatable, Sequence[InstrumentModuleType]):
class ChannelTuple(MetadatableWithName, Sequence[InstrumentModuleType]):
"""
Container for channelized parameters that allows for sweeps over
all channels, as well as addressing of individual channels.
Expand Down Expand Up @@ -273,6 +273,34 @@ def __add__(self: T, other: ChannelTuple) -> T:
snapshotable=self._snapshotable,
)

@property
def short_name(self) -> str:
return self._name

@property
def full_name(self) -> str:
return "_".join(self.name_parts)

@property
def name_parts(self) -> list[str]:
"""
List of the parts that make up the full name of this function
"""
if self._parent is not None:
name_parts = getattr(self._parent, "name_parts", [])
if name_parts == []:
# add fallback for the case where someone has bound
# the function to something that is not an instrument
# but perhaps it has a name anyway?
name = getattr(self._parent, "name", None)
if name is not None:
name_parts = [name]
else:
name_parts = []

name_parts.append(self.short_name)
return name_parts

def index(
self, obj: InstrumentModuleType, start: int = 0, stop: int = sys.maxsize
) -> int:
Expand Down
79 changes: 77 additions & 2 deletions qcodes/instrument/instrument_base.py
Expand Up @@ -10,7 +10,7 @@
import numpy as np

from qcodes.logger import get_instrument_logger
from qcodes.metadatable import Metadatable
from qcodes.metadatable import Metadatable, MetadatableWithName
from qcodes.parameters import Function, Parameter, ParameterBase
from qcodes.utils import DelegateAttributes, full_class

Expand All @@ -23,7 +23,7 @@
log = logging.getLogger(__name__)


class InstrumentBase(Metadatable, DelegateAttributes):
class InstrumentBase(MetadatableWithName, DelegateAttributes):
"""
Base class for all QCodes instruments and instrument channels

Expand Down Expand Up @@ -234,6 +234,81 @@ def add_submodule(
else:
self.instrument_modules[name] = submodule

def get_component(self, full_name: str) -> MetadatableWithName:
"""
Recursively get a component of the instrument by full_name.

Args:
name: The name of the component to get.

Returns:
The component with the given name.

Raises:
KeyError: If the component does not exist.
"""
name_parts = full_name.split("_")
name_parts.reverse()

component = self._get_component_by_name(name_parts.pop(), name_parts)
return component

def _get_component_by_name(
self, potential_top_level_name: str, remaining_name_parts: list[str]
) -> MetadatableWithName:
component: MetadatableWithName | None = None

sub_component_name_map = {
sub_component.short_name: sub_component
for sub_component in self.submodules.values()
}

if potential_top_level_name in self.parameters:
component = self.parameters[potential_top_level_name]
elif potential_top_level_name in self.functions:
component = self.functions[potential_top_level_name]
elif potential_top_level_name in self.submodules:
# recursive call on found component
component = self.submodules[potential_top_level_name]
if len(remaining_name_parts) > 0:
remaining_name_parts.reverse()
remaining_name = "_".join(remaining_name_parts)
component = component.get_component(remaining_name)
remaining_name_parts = []
elif potential_top_level_name in sub_component_name_map:
component = sub_component_name_map[potential_top_level_name]
if len(remaining_name_parts) > 0:
remaining_name_parts.reverse()
remaining_name = "_".join(remaining_name_parts)
component = component.get_component(remaining_name)
remaining_name_parts = []

if component is not None:
if len(remaining_name_parts) == 0:
return component

remaining_name_parts.reverse()
raise KeyError(
f"Found component {component.full_name} but could not match "
f"{'_'.join(remaining_name_parts)} part."
)

if len(remaining_name_parts) == 0:
raise KeyError(
f"Found component {self.full_name} but could not "
f"match {potential_top_level_name} part."
)

new_potential_top_level_name = (
f"{potential_top_level_name}_{remaining_name_parts.pop()}"
)
remaining_name_parts.reverse()
component = self._get_component_by_name(
new_potential_top_level_name, remaining_name_parts
)

return component

def snapshot_base(
self,
update: bool | None = False,
Expand Down
4 changes: 2 additions & 2 deletions qcodes/metadatable/__init__.py
@@ -1,3 +1,3 @@
from .metadatable_base import Metadatable
from .metadatable_base import Metadatable, MetadatableWithName

__all__ = ["Metadatable"]
__all__ = ["Metadatable", "MetadatableWithName"]
22 changes: 22 additions & 0 deletions qcodes/metadatable/metadatable_base.py
@@ -1,3 +1,4 @@
from abc import abstractmethod
from typing import Any, Dict, Mapping, Optional, Sequence

from qcodes.utils import deep_update
Expand Down Expand Up @@ -57,3 +58,24 @@ def snapshot_base(
Override this with the primary information for a subclass.
"""
return {}


class MetadatableWithName(Metadatable):
"""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
consistent interface."""

@property
@abstractmethod
def short_name(self) -> str:
"""
Name excluding name of any parent that this object is bound to.
"""

@property
@abstractmethod
def full_name(self) -> str:
"""
Name including name of any parent that this object is bound to separated by '_'.
"""
45 changes: 43 additions & 2 deletions qcodes/parameters/function.py
Expand Up @@ -3,7 +3,7 @@
from collections.abc import Callable, Sequence
from typing import TYPE_CHECKING, Any

from qcodes.metadatable import Metadatable
from qcodes.metadatable import MetadatableWithName
from qcodes.validators import Validator, validate_all

from .command import Command
Expand All @@ -12,7 +12,7 @@
from qcodes.instrument import InstrumentBase


class Function(Metadatable):
class Function(MetadatableWithName):
"""
Defines a function that an instrument can execute.

Expand Down Expand Up @@ -170,3 +170,44 @@ def get_attrs(self) -> list[str]:
Returns (list): __doc__, _args, and _arg_count get proxied
"""
return ["__doc__", "_args", "_arg_count"]

@property
def short_name(self) -> str:
"""
Name excluding name of any instrument that this function may be
bound to.
"""
return self.name

@property
def full_name(self) -> str:
"""
Name of the function including the name of the instrument and
submodule that the function may be bound to. The names are separated
by underscores, like this: ``instrument_submodule_function``.
"""
return "_".join(self.name_parts)

@property
def name_parts(self) -> list[str]:
"""
List of the parts that make up the full name of this function
"""
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 function 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 instrument(self) -> InstrumentBase | None:
return self._instrument
4 changes: 2 additions & 2 deletions qcodes/parameters/parameter_base.py
Expand Up @@ -10,7 +10,7 @@
from types import TracebackType
from typing import TYPE_CHECKING, Any, overload

from qcodes.metadatable import Metadatable
from qcodes.metadatable import Metadatable, MetadatableWithName
from qcodes.utils import DelegateAttributes, full_class, qcodes_abstractmethod
from qcodes.validators import Enum, Ints, Validator

Expand Down Expand Up @@ -83,7 +83,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(Metadatable):
class ParameterBase(MetadatableWithName):
"""
Shared behavior for all parameters. Not intended to be used
directly, normally you should use ``Parameter``, ``ArrayParameter``,
Expand Down