pyglaze 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyglaze/__init__.py +1 -0
- pyglaze/datamodels/__init__.py +4 -0
- pyglaze/datamodels/pulse.py +551 -0
- pyglaze/datamodels/waveform.py +165 -0
- pyglaze/device/__init__.py +15 -0
- pyglaze/device/_delayunit_data/carmen-nonuniform-2023-10-20.pickle +0 -0
- pyglaze/device/_delayunit_data/g1-linearized-2023-04-04.pickle +0 -0
- pyglaze/device/_delayunit_data/g2-linearized-2023-04-04.pickle +0 -0
- pyglaze/device/_delayunit_data/g2-nonuniform-2023-04-04.pickle +0 -0
- pyglaze/device/_delayunit_data/mock_delay.pickle +0 -0
- pyglaze/device/ampcom.py +447 -0
- pyglaze/device/configuration.py +266 -0
- pyglaze/device/delayunit.py +151 -0
- pyglaze/device/identifiers.py +41 -0
- pyglaze/devtools/__init__.py +3 -0
- pyglaze/devtools/mock_device.py +367 -0
- pyglaze/devtools/thz_pulse.py +35 -0
- pyglaze/helpers/__init__.py +0 -0
- pyglaze/helpers/types.py +20 -0
- pyglaze/helpers/utilities.py +80 -0
- pyglaze/interpolation/__init__.py +3 -0
- pyglaze/interpolation/interpolation.py +24 -0
- pyglaze/py.typed +0 -0
- pyglaze/scanning/__init__.py +4 -0
- pyglaze/scanning/_asyncscanner.py +146 -0
- pyglaze/scanning/client.py +59 -0
- pyglaze/scanning/scanner.py +256 -0
- pyglaze-0.1.0.dist-info/LICENSE +28 -0
- pyglaze-0.1.0.dist-info/METADATA +82 -0
- pyglaze-0.1.0.dist-info/RECORD +32 -0
- pyglaze-0.1.0.dist-info/WHEEL +5 -0
- pyglaze-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar, TypeVar
|
|
7
|
+
|
|
8
|
+
from .delayunit import validate_delayunit
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T", bound="DeviceConfiguration")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Interval:
|
|
18
|
+
"""An interval with a lower and upper bounds between 0 and 1 to scan."""
|
|
19
|
+
|
|
20
|
+
lower: float
|
|
21
|
+
upper: float
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def length(self: Interval) -> float:
|
|
25
|
+
"""The length of the interval."""
|
|
26
|
+
return abs(self.upper - self.lower)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_dict(cls: type[Interval], d: dict) -> Interval:
|
|
30
|
+
"""Create an instance of the Interval class from a dictionary.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
d (dict): The dictionary containing the interval data.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Interval: An instance of the Interval class.
|
|
37
|
+
"""
|
|
38
|
+
return cls(**d)
|
|
39
|
+
|
|
40
|
+
def __post_init__(self: Interval) -> None: # noqa: D105
|
|
41
|
+
if not 0.0 <= self.lower <= 1.0:
|
|
42
|
+
msg = "Interval: Bounds must be between 0 and 1"
|
|
43
|
+
raise ValueError(msg)
|
|
44
|
+
if not 0.0 <= self.upper <= 1.0:
|
|
45
|
+
msg = "Interval: Bounds must be between 0 and 1"
|
|
46
|
+
raise ValueError(msg)
|
|
47
|
+
if self.upper == self.lower:
|
|
48
|
+
msg = "Interval: Bounds cannot be equal"
|
|
49
|
+
raise ValueError(msg)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DeviceConfiguration(ABC):
|
|
53
|
+
"""Base class for device configurations."""
|
|
54
|
+
|
|
55
|
+
amp_timeout_seconds: float
|
|
56
|
+
amp_port: str
|
|
57
|
+
amp_baudrate: ClassVar[int]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def _sweep_length_ms(self: DeviceConfiguration) -> float:
|
|
62
|
+
"""The length of the sweep in milliseconds."""
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def save(self: DeviceConfiguration, path: Path) -> str:
|
|
66
|
+
"""Save a DeviceConfiguration to a file."""
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def from_dict(cls: type[T], amp_config: dict) -> T:
|
|
71
|
+
"""Create a DeviceConfiguration from a dict."""
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def load(cls: type[T], file_path: Path) -> T:
|
|
76
|
+
"""Load a DeviceConfiguration from a file."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class ForceDeviceConfiguration(DeviceConfiguration):
|
|
81
|
+
"""Represents a configuration that can be sent to the lock-in amp for scans.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
amp_port: The name of the serial port the amp is connected to.
|
|
85
|
+
sweep_length_ms: The length of the sweep in milliseconds.
|
|
86
|
+
delayunit: Name of the delay calculator.
|
|
87
|
+
scan_intervals: The intervals to scan.
|
|
88
|
+
integration_periods: The number of integration periods to use.
|
|
89
|
+
modulation_frequency: The frequency of the modulation in Hz.
|
|
90
|
+
dac_lower_bound: The lower bound of the modulation voltage in bits.
|
|
91
|
+
dac_upper_bound: The upper bound of the modulation voltage in bits.
|
|
92
|
+
min_modulation_voltage: The minimum modulation voltage in volts.
|
|
93
|
+
max_modulation_voltage: The maximum modulation voltage in volts.
|
|
94
|
+
modulation_waveform: The waveform to use for modulation.
|
|
95
|
+
amp_timeout_seconds: The timeout for the amp in seconds.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
amp_port: str
|
|
99
|
+
sweep_length_ms: float
|
|
100
|
+
delayunit: str
|
|
101
|
+
scan_intervals: list[Interval] = field(default_factory=lambda: [Interval(0.0, 1.0)])
|
|
102
|
+
integration_periods: int = 100
|
|
103
|
+
modulation_frequency: int = 10000 # Hz
|
|
104
|
+
dac_lower_bound: int = 6400
|
|
105
|
+
dac_upper_bound: int = 59300
|
|
106
|
+
min_modulation_voltage: float = -1.0 # V
|
|
107
|
+
max_modulation_voltage: float = 0.5 # V
|
|
108
|
+
modulation_waveform: str = "square"
|
|
109
|
+
amp_timeout_seconds: float = 0.05
|
|
110
|
+
|
|
111
|
+
amp_baudrate: ClassVar[int] = 1200000 # bit/s
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def _sweep_length_ms(self: ForceDeviceConfiguration) -> float:
|
|
115
|
+
return self.sweep_length_ms
|
|
116
|
+
|
|
117
|
+
def __post_init__(self: ForceDeviceConfiguration) -> None: # noqa: D105
|
|
118
|
+
validate_delayunit(self.delayunit)
|
|
119
|
+
|
|
120
|
+
def save(self: ForceDeviceConfiguration, path: Path) -> str:
|
|
121
|
+
"""Save a DeviceConfiguration to a file.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
path: The path to save the configuration to.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
str: Final path component of the saved file, without the extension.
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
with path.open("w") as f:
|
|
131
|
+
json.dump(asdict(self), f, indent=4, sort_keys=True)
|
|
132
|
+
|
|
133
|
+
return path.stem
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def from_dict(
|
|
137
|
+
cls: type[ForceDeviceConfiguration], amp_config: dict
|
|
138
|
+
) -> ForceDeviceConfiguration:
|
|
139
|
+
"""Create a DeviceConfiguration from a dict.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
amp_config: An amp configuration in dict form.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ValueError: If the dictionary is empty.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
DeviceConfiguration: A DeviceConfiguration object.
|
|
149
|
+
"""
|
|
150
|
+
return _config_w_intervals_from_dict(cls, amp_config)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def load(
|
|
154
|
+
cls: type[ForceDeviceConfiguration], file_path: Path
|
|
155
|
+
) -> ForceDeviceConfiguration:
|
|
156
|
+
"""Load a DeviceConfiguration from a file.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
file_path: The path to the file to load.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
DeviceConfiguration: A DeviceConfiguration object.
|
|
163
|
+
"""
|
|
164
|
+
with file_path.open() as f:
|
|
165
|
+
configuration_dict = json.load(f)
|
|
166
|
+
return cls.from_dict(configuration_dict)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class LeDeviceConfiguration(DeviceConfiguration):
|
|
171
|
+
"""Represents a configuration that can be sent to a Le-type lock-in amp for scans.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
amp_port: The name of the serial port the amp is connected to.
|
|
175
|
+
delayunit: Name of the delay calculator.
|
|
176
|
+
use_ema: Whether to use en exponentially moving average filter during lockin detection.
|
|
177
|
+
n_points: The number of points to scan.
|
|
178
|
+
scan_intervals: The intervals to scan.
|
|
179
|
+
integration_periods: The number of integration periods per datapoint to use.
|
|
180
|
+
amp_timeout_seconds: The timeout for the connection to the amp in seconds.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
amp_port: str
|
|
184
|
+
delayunit: str
|
|
185
|
+
use_ema: bool = True
|
|
186
|
+
n_points: int = 1000
|
|
187
|
+
scan_intervals: list[Interval] = field(default_factory=lambda: [Interval(0.0, 1.0)])
|
|
188
|
+
integration_periods: int = 10
|
|
189
|
+
amp_timeout_seconds: float = 0.2
|
|
190
|
+
|
|
191
|
+
modulation_frequency: ClassVar[int] = 10000 # Hz
|
|
192
|
+
fs_dac_lower_bound: ClassVar[int] = 300
|
|
193
|
+
fs_dac_upper_bound: ClassVar[int] = 3700
|
|
194
|
+
amp_baudrate: ClassVar[int] = 1000000 # bit/s
|
|
195
|
+
|
|
196
|
+
def __post_init__(self: LeDeviceConfiguration) -> None: # noqa: D105
|
|
197
|
+
validate_delayunit(self.delayunit)
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def _sweep_length_ms(self: LeDeviceConfiguration) -> float:
|
|
201
|
+
return self.n_points * self._time_constant_ms
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def _time_constant_ms(self: LeDeviceConfiguration) -> float:
|
|
205
|
+
return 1e3 * self.integration_periods / self.modulation_frequency
|
|
206
|
+
|
|
207
|
+
def save(self: LeDeviceConfiguration, path: Path) -> str:
|
|
208
|
+
"""Save a LeDeviceConfiguration to a file.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
path: The path to save the configuration to.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
str: Final path component of the saved file, without the extension.
|
|
215
|
+
|
|
216
|
+
"""
|
|
217
|
+
with path.open("w") as f:
|
|
218
|
+
json.dump(asdict(self), f, indent=4, sort_keys=True)
|
|
219
|
+
|
|
220
|
+
return path.stem
|
|
221
|
+
|
|
222
|
+
@classmethod
|
|
223
|
+
def from_dict(
|
|
224
|
+
cls: type[LeDeviceConfiguration], amp_config: dict
|
|
225
|
+
) -> LeDeviceConfiguration:
|
|
226
|
+
"""Create a LeDeviceConfiguration from a dict.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
amp_config: An amp configuration in dict form.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
ValueError: If the dictionary is empty.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
DeviceConfiguration: A DeviceConfiguration object.
|
|
236
|
+
"""
|
|
237
|
+
return _config_w_intervals_from_dict(cls, amp_config)
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def load(
|
|
241
|
+
cls: type[LeDeviceConfiguration], file_path: Path
|
|
242
|
+
) -> LeDeviceConfiguration:
|
|
243
|
+
"""Load a LeDeviceConfiguration from a file.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
file_path: The path to the file to load.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
DeviceConfiguration: A DeviceConfiguration object.
|
|
250
|
+
"""
|
|
251
|
+
with file_path.open() as f:
|
|
252
|
+
configuration_dict = json.load(f)
|
|
253
|
+
return cls.from_dict(configuration_dict)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
C = TypeVar("C", LeDeviceConfiguration, ForceDeviceConfiguration)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _config_w_intervals_from_dict(cls: type[C], amp_config: dict) -> C:
|
|
260
|
+
if amp_config:
|
|
261
|
+
config = cls(**amp_config)
|
|
262
|
+
config.scan_intervals = [Interval.from_dict(d) for d in config.scan_intervals] # type: ignore[arg-type]
|
|
263
|
+
return config
|
|
264
|
+
|
|
265
|
+
msg = "'amp_config' is empty."
|
|
266
|
+
raise ValueError(msg)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pickle
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Callable, cast
|
|
9
|
+
from uuid import UUID, uuid4
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pyglaze.helpers.types import FloatArray
|
|
15
|
+
|
|
16
|
+
__all__ = ["UniformDelay", "NonuniformDelay", "list_delayunits", "load_delayunit"]
|
|
17
|
+
|
|
18
|
+
_DELAYUNITS_PATH = Path(__file__).parent / "_delayunit_data"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_delayunit(name: str) -> None:
|
|
22
|
+
delayunits = list_delayunits()
|
|
23
|
+
if name not in delayunits:
|
|
24
|
+
msg = f"Unknown delayunit '{name}'. Valid options are: {', '.join(delayunits)}."
|
|
25
|
+
raise ValueError(msg)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def list_delayunits() -> list[str]:
|
|
29
|
+
"""List all available delayunits.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A list of all available delayunits.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
return [delayunit.stem for delayunit in _DELAYUNITS_PATH.iterdir()]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_delayunit(name: str) -> Delay:
|
|
39
|
+
"""Load a delayunit by name.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
name: The name of the delayunit to load.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The loaded delayunit.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
return _load_delayunit_from_path(_DELAYUNITS_PATH / f"{name}.pickle")
|
|
49
|
+
except FileNotFoundError as e:
|
|
50
|
+
msg = f"Unknown delayunit requested ('{name}'). Known units are: {list_delayunits()}"
|
|
51
|
+
raise NameError(msg) from e
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _load_delayunit_from_path(path: Path) -> Delay:
|
|
55
|
+
with Path(path).open("rb") as f:
|
|
56
|
+
_dict: dict = pickle.load(f)
|
|
57
|
+
delay_type = _dict.pop("type")
|
|
58
|
+
return cast(Delay, globals()[delay_type](**_dict))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class Delay(ABC):
|
|
63
|
+
friendly_name: str
|
|
64
|
+
unique_id: UUID
|
|
65
|
+
creation_time: datetime
|
|
66
|
+
|
|
67
|
+
def __call__(self: Delay, x: FloatArray) -> FloatArray:
|
|
68
|
+
if np.max(x) > 1.0 or np.min(x) < 0.0:
|
|
69
|
+
msg = "All values of 'x' must be between 0 and 1."
|
|
70
|
+
raise ValueError(msg)
|
|
71
|
+
|
|
72
|
+
return self._call(x)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def filename(self: Delay) -> str:
|
|
76
|
+
return f"{self.friendly_name}-{self.creation_time.strftime('%Y-%m-%d')}.pickle"
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def _call(self: Delay, x: FloatArray) -> FloatArray: ...
|
|
80
|
+
|
|
81
|
+
def save(self: Delay, path: Path) -> None:
|
|
82
|
+
with Path(path).open("wb") as f:
|
|
83
|
+
pickle.dump({"type": self.__class__.__name__, **asdict(self)}, f)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class UniformDelay(Delay):
|
|
88
|
+
"""A delay calculator that calculates equidisant delays."""
|
|
89
|
+
|
|
90
|
+
time_window: float
|
|
91
|
+
|
|
92
|
+
def _call(self: UniformDelay, x: FloatArray) -> FloatArray:
|
|
93
|
+
return x * self.time_window
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def new(cls: type[UniformDelay], time_window: float, friendly_name: str) -> Delay:
|
|
97
|
+
"""Create a new Delay object.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
time_window: The time window for the delay.
|
|
101
|
+
friendly_name: The friendly name of the delay.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Delay: The newly created Delay object.
|
|
105
|
+
"""
|
|
106
|
+
return cls(
|
|
107
|
+
friendly_name=friendly_name,
|
|
108
|
+
unique_id=uuid4(),
|
|
109
|
+
creation_time=datetime.now(), # noqa: DTZ005
|
|
110
|
+
time_window=time_window,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class NonuniformDelay(Delay):
|
|
116
|
+
"""A delay calculator that calculates non-equidistant delays."""
|
|
117
|
+
|
|
118
|
+
time_window: float
|
|
119
|
+
residual_interpolator: Callable[[FloatArray], FloatArray]
|
|
120
|
+
|
|
121
|
+
def _call(self: NonuniformDelay, x: FloatArray) -> FloatArray:
|
|
122
|
+
return (
|
|
123
|
+
x * self.time_window
|
|
124
|
+
+ self.residual_interpolator(x)
|
|
125
|
+
- self.residual_interpolator(np.asarray(0.0))
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def new(
|
|
130
|
+
cls: type[NonuniformDelay],
|
|
131
|
+
friendly_name: str,
|
|
132
|
+
time_window: float,
|
|
133
|
+
residual_interpolator: Callable[[FloatArray], FloatArray],
|
|
134
|
+
) -> Delay:
|
|
135
|
+
"""Create a new NonuniformDelay object.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
friendly_name: The friendly name of the NonuniformDelay object.
|
|
139
|
+
time_window: The time window of the pulse.
|
|
140
|
+
residual_interpolator: a residual interpolator for calculating the nonuniform part of the delay.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Delay: The newly created Delay object.
|
|
144
|
+
"""
|
|
145
|
+
return cls(
|
|
146
|
+
friendly_name=friendly_name,
|
|
147
|
+
unique_id=uuid4(),
|
|
148
|
+
creation_time=datetime.now(), # noqa: DTZ005
|
|
149
|
+
time_window=time_window,
|
|
150
|
+
residual_interpolator=residual_interpolator,
|
|
151
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Literal, get_args
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from typing_extensions import TypeAlias
|
|
5
|
+
|
|
6
|
+
DeviceName: TypeAlias = Literal["glaze1", "glaze2", "carmen"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _device_ids() -> dict[DeviceName, UUID]:
|
|
10
|
+
return {
|
|
11
|
+
"glaze1": UUID("5042dbda-e9bc-4216-a614-ac56d0a32023"),
|
|
12
|
+
"glaze2": UUID("66fa482a-1ef4-4076-a883-72d7bf43e151"),
|
|
13
|
+
"carmen": UUID("6a54db26-fa88-4146-b04f-b84b945bfea8"),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def list_devices() -> list[DeviceName]:
|
|
18
|
+
"""List all available devices.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
A list of all available devices.
|
|
22
|
+
"""
|
|
23
|
+
return list(_device_ids().keys())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_device_id(name: DeviceName) -> UUID:
|
|
27
|
+
"""Get the UUID of a device by its name.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
name: The name of the device.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Unique identifier of a device.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
return _device_ids()[name]
|
|
37
|
+
except KeyError as e:
|
|
38
|
+
msg = (
|
|
39
|
+
f"Device {name} does not exist. Possible values are {get_args(DeviceName)}"
|
|
40
|
+
)
|
|
41
|
+
raise ValueError(msg) from e
|