quelware-client 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.
- quelware_client/__init__.py +3 -0
- quelware_client/client/__init__.py +4 -0
- quelware_client/client/_grpc.py +61 -0
- quelware_client/client/_standalone_grpc.py +48 -0
- quelware_client/client/helpers/__init__.py +0 -0
- quelware_client/client/helpers/instrument_resolver/__init__.py +58 -0
- quelware_client/client/helpers/sequencer/__init__.py +248 -0
- quelware_client/core/__init__.py +5 -0
- quelware_client/core/_agent_container.py +72 -0
- quelware_client/core/_client.py +99 -0
- quelware_client/core/_session.py +151 -0
- quelware_client/core/_utils.py +17 -0
- quelware_client/core/exceptions/__init__.py +74 -0
- quelware_client/core/instrument_driver/__init__.py +103 -0
- quelware_client/core/interfaces/__init__.py +0 -0
- quelware_client/core/interfaces/instrument_agent/__init__.py +55 -0
- quelware_client/core/interfaces/resource_agent/__init__.py +34 -0
- quelware_client/core/interfaces/session_agent/__init__.py +21 -0
- quelware_client/core/interfaces/system_configuration_agent/__init__.py +11 -0
- quelware_client/core/trigger_count_proposer/__init__.py +26 -0
- quelware_client/infra/__init__.py +0 -0
- quelware_client/infra/instrument_agent_grpc/__init__.py +111 -0
- quelware_client/infra/resource_agent_grpc/__init__.py +74 -0
- quelware_client/infra/session_agent_grpc/__init__.py +82 -0
- quelware_client/infra/system_configuration_agent_grpc/__init__.py +22 -0
- quelware_client/py.typed +0 -0
- quelware_client/testing/__init__.py +3 -0
- quelware_client/testing/instrument_agent_mock/__init__.py +64 -0
- quelware_client/testing/mock_client.py +34 -0
- quelware_client/testing/resource_agent_mock/__init__.py +125 -0
- quelware_client/testing/session_agent_mock/__init__.py +32 -0
- quelware_client/testing/system_configuration_agent_mock/__init__.py +16 -0
- quelware_client-0.1.0.dist-info/METADATA +63 -0
- quelware_client-0.1.0.dist-info/RECORD +36 -0
- quelware_client-0.1.0.dist-info/WHEEL +4 -0
- quelware_client-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from grpclib.client import Channel
|
|
2
|
+
from quelware_core.entities.unit import UnitLabel
|
|
3
|
+
|
|
4
|
+
from quelware_client.core import AgentContainer, AgentFactory, QuelwareClient
|
|
5
|
+
from quelware_client.core.interfaces.instrument_agent import InstrumentAgent
|
|
6
|
+
from quelware_client.core.interfaces.resource_agent import ResourceAgent
|
|
7
|
+
from quelware_client.core.interfaces.session_agent import SessionAgent
|
|
8
|
+
from quelware_client.core.interfaces.system_configuration_agent import (
|
|
9
|
+
SystemConfigurationAgent,
|
|
10
|
+
)
|
|
11
|
+
from quelware_client.infra.instrument_agent_grpc import InstrumentAgentGrpc
|
|
12
|
+
from quelware_client.infra.resource_agent_grpc import (
|
|
13
|
+
ResourceAgentGrpc,
|
|
14
|
+
)
|
|
15
|
+
from quelware_client.infra.session_agent_grpc import SessionAgentGrpc
|
|
16
|
+
from quelware_client.infra.system_configuration_agent_grpc import (
|
|
17
|
+
SystemConfigurationAgentGrpc,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _create_default_resource_agent_factory(channel):
|
|
22
|
+
def _default_resource_agent_factory(ul: UnitLabel):
|
|
23
|
+
return ResourceAgentGrpc(channel, metadata={"x-unit-label": str(ul)})
|
|
24
|
+
|
|
25
|
+
return _default_resource_agent_factory
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _create_default_instrument_agent_factory(channel):
|
|
29
|
+
def _default_command_agent_factory(ul: UnitLabel):
|
|
30
|
+
return InstrumentAgentGrpc(channel, metadata={"x-unit-label": str(ul)})
|
|
31
|
+
|
|
32
|
+
return _default_command_agent_factory
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_quelware_client( # noqa: PLR0913
|
|
36
|
+
endpoint: str = "localhost",
|
|
37
|
+
port: int = 50051,
|
|
38
|
+
session_agent: SessionAgent | None = None,
|
|
39
|
+
configuration_agent: SystemConfigurationAgent | None = None,
|
|
40
|
+
resource_agent_factory: AgentFactory[ResourceAgent] | None = None,
|
|
41
|
+
instrument_agent_factory: AgentFactory[InstrumentAgent] | None = None,
|
|
42
|
+
):
|
|
43
|
+
channel = Channel(endpoint, port)
|
|
44
|
+
|
|
45
|
+
if resource_agent_factory is None:
|
|
46
|
+
resource_agent_factory = _create_default_resource_agent_factory(channel)
|
|
47
|
+
|
|
48
|
+
if instrument_agent_factory is None:
|
|
49
|
+
instrument_agent_factory = _create_default_instrument_agent_factory(channel)
|
|
50
|
+
|
|
51
|
+
agent_container = AgentContainer()
|
|
52
|
+
if session_agent is None:
|
|
53
|
+
agent_container.session = SessionAgentGrpc(channel)
|
|
54
|
+
if configuration_agent is None:
|
|
55
|
+
agent_container.system_configuration = SystemConfigurationAgentGrpc(channel)
|
|
56
|
+
return QuelwareClient(
|
|
57
|
+
agent=agent_container,
|
|
58
|
+
resource_agent_factory=resource_agent_factory,
|
|
59
|
+
instrument_agent_factory=instrument_agent_factory,
|
|
60
|
+
close_handlers=[channel.close],
|
|
61
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from grpclib.client import Channel
|
|
4
|
+
from quelware_core.entities.unit import UnitLabel, UnitStatus
|
|
5
|
+
|
|
6
|
+
from quelware_client.core import AgentContainer, QuelwareClient
|
|
7
|
+
from quelware_client.infra.instrument_agent_grpc import InstrumentAgentGrpc
|
|
8
|
+
from quelware_client.infra.resource_agent_grpc import ResourceAgentGrpc
|
|
9
|
+
from quelware_client.testing.session_agent_mock import SessionAgentMock
|
|
10
|
+
from quelware_client.testing.system_configuration_agent_mock import (
|
|
11
|
+
SystemConfigurationAgentMock,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_standalone_client(
|
|
18
|
+
endpoint: str = "localhost", port: int = 50051, unit_label: str = "mock-unit"
|
|
19
|
+
):
|
|
20
|
+
"""Create a standalone client.
|
|
21
|
+
|
|
22
|
+
Note that the target worker server must be running with lock checking
|
|
23
|
+
disabled to use this client.
|
|
24
|
+
"""
|
|
25
|
+
channel = Channel(endpoint, port)
|
|
26
|
+
target_unit = UnitLabel(unit_label)
|
|
27
|
+
|
|
28
|
+
conf_agent = SystemConfigurationAgentMock({target_unit: UnitStatus.ACTIVE})
|
|
29
|
+
session_agent = SessionAgentMock()
|
|
30
|
+
|
|
31
|
+
def resource_agent_factory(ul: UnitLabel):
|
|
32
|
+
return ResourceAgentGrpc(channel, metadata={"x-unit-label": str(ul)})
|
|
33
|
+
|
|
34
|
+
def instrument_agent_factory(ul: UnitLabel):
|
|
35
|
+
return InstrumentAgentGrpc(channel, metadata={"x-unit-label": str(ul)})
|
|
36
|
+
|
|
37
|
+
agent_container = AgentContainer()
|
|
38
|
+
agent_container.session = session_agent
|
|
39
|
+
agent_container.system_configuration = conf_agent
|
|
40
|
+
|
|
41
|
+
logger.warning("NOTE: Standalone client is for testing purposes.")
|
|
42
|
+
|
|
43
|
+
return QuelwareClient(
|
|
44
|
+
agent=agent_container,
|
|
45
|
+
resource_agent_factory=resource_agent_factory,
|
|
46
|
+
instrument_agent_factory=instrument_agent_factory,
|
|
47
|
+
close_handlers=[channel.close],
|
|
48
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from quelware_core.entities.instrument import InstrumentInfo
|
|
5
|
+
from quelware_core.entities.resource import (
|
|
6
|
+
ResourceCategory,
|
|
7
|
+
ResourceId,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from quelware_client.core import QuelwareClient
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InstrumentResolver:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self._id_to_inst_info: dict[ResourceId, InstrumentInfo] = {}
|
|
18
|
+
self._alias_to_id: dict[str, ResourceId] = {}
|
|
19
|
+
|
|
20
|
+
async def refresh(self, client: QuelwareClient):
|
|
21
|
+
resource_infos = list(
|
|
22
|
+
rinfo
|
|
23
|
+
for rinfo in await client.list_resource_infos()
|
|
24
|
+
if rinfo.category is ResourceCategory.INSTRUMENT
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
coros = [client.get_instrument_info(rinfo.id) for rinfo in resource_infos]
|
|
28
|
+
|
|
29
|
+
inst_infos: list[InstrumentInfo] = await asyncio.gather(*coros)
|
|
30
|
+
|
|
31
|
+
new_id_to_inst_info = {}
|
|
32
|
+
new_alias_to_id = {}
|
|
33
|
+
for inst_info in inst_infos:
|
|
34
|
+
new_id_to_inst_info[inst_info.id] = inst_info
|
|
35
|
+
new_alias_to_id[inst_info.definition.alias] = inst_info.id
|
|
36
|
+
|
|
37
|
+
self._id_to_inst_info = new_id_to_inst_info
|
|
38
|
+
self._alias_to_id = new_alias_to_id
|
|
39
|
+
logger.info(f"{len(self._id_to_inst_info)} instruments has been registered.")
|
|
40
|
+
|
|
41
|
+
def find_inst_info_by_id(self, instrument_id: ResourceId) -> InstrumentInfo:
|
|
42
|
+
if instrument_id not in self._id_to_inst_info:
|
|
43
|
+
raise ValueError(f"Instrument with id '{instrument_id}' not found.")
|
|
44
|
+
return self._id_to_inst_info[instrument_id]
|
|
45
|
+
|
|
46
|
+
def find_inst_info_by_alias(self, alias: str) -> InstrumentInfo:
|
|
47
|
+
return self.find_inst_info_by_id(self._resolve_single(alias))
|
|
48
|
+
|
|
49
|
+
def _resolve_single(self, alias) -> ResourceId:
|
|
50
|
+
if alias not in self._alias_to_id:
|
|
51
|
+
raise ValueError(f"Instrument with alias '{alias}' not found.")
|
|
52
|
+
return self._alias_to_id[alias]
|
|
53
|
+
|
|
54
|
+
def resolve(self, aliases: list[str]) -> list[ResourceId]:
|
|
55
|
+
return list(self._resolve_single(alias) for alias in aliases)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = ["InstrumentResolver"]
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import math
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import numpy.typing as npt
|
|
8
|
+
from quelware_core.entities.directives import (
|
|
9
|
+
CaptureWindow,
|
|
10
|
+
SetFixedTimeline,
|
|
11
|
+
WaveformEvent,
|
|
12
|
+
)
|
|
13
|
+
from quelware_core.entities.waveform.sampled import IqArray, IqWaveform
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class _SequencerEvent:
|
|
20
|
+
waveform_name: str
|
|
21
|
+
start_offset_ns: float
|
|
22
|
+
gain: float
|
|
23
|
+
phase_offset_deg: float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class _SequencerCaptureWindow:
|
|
28
|
+
name: str
|
|
29
|
+
start_offset_ns: float
|
|
30
|
+
length_ns: float
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class _Waveform:
|
|
35
|
+
sampling_period_ns: float
|
|
36
|
+
iq_array: IqArray
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class _AliasBinding:
|
|
41
|
+
sampling_period_fs: int
|
|
42
|
+
step_samples: int
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Sequencer:
|
|
46
|
+
def __init__(
|
|
47
|
+
self, default_sampling_period_ns: float, enforce_sample_grid: bool = True
|
|
48
|
+
):
|
|
49
|
+
self._waveform_library: dict[str, _Waveform] = {}
|
|
50
|
+
self._alias_to_events: dict[str, list[_SequencerEvent]] = defaultdict(list)
|
|
51
|
+
self._alias_to_capwin: dict[str, list[_SequencerCaptureWindow]] = defaultdict(
|
|
52
|
+
list
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
self._default_sampling_period_ns: float = default_sampling_period_ns
|
|
56
|
+
self._iterations: int = 1
|
|
57
|
+
|
|
58
|
+
self._bindings: dict[str, _AliasBinding] = {}
|
|
59
|
+
self._enforce_sample_grid: bool = enforce_sample_grid
|
|
60
|
+
self._length_ns: float = 0.0
|
|
61
|
+
|
|
62
|
+
def bind(self, alias: str, sampling_period_fs: int, step_samples: int):
|
|
63
|
+
self._bindings[alias] = _AliasBinding(
|
|
64
|
+
sampling_period_fs=sampling_period_fs,
|
|
65
|
+
step_samples=step_samples,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def _check_and_convert_to_samples(
|
|
69
|
+
self, alias: str, time_ns: float, name_for_log: str
|
|
70
|
+
) -> int:
|
|
71
|
+
if alias not in self._bindings:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Alias '{alias}' is not bound to the sequencer. Call bind() first."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
period_fs = self._bindings[alias].sampling_period_fs
|
|
77
|
+
time_fs = time_ns * 1e6
|
|
78
|
+
samples = time_fs / period_fs
|
|
79
|
+
rounded_samples = round(samples)
|
|
80
|
+
|
|
81
|
+
if abs(samples - rounded_samples) > 0.001:
|
|
82
|
+
msg = (
|
|
83
|
+
f"{name_for_log} ({time_ns} ns) is not a multiple of sampling period "
|
|
84
|
+
f"({period_fs} fs) for alias '{alias}'. Calculated samples: {samples}"
|
|
85
|
+
)
|
|
86
|
+
if self._enforce_sample_grid:
|
|
87
|
+
raise ValueError(msg)
|
|
88
|
+
else:
|
|
89
|
+
logger.warning(f"{msg}. Rounding to {rounded_samples} samples.")
|
|
90
|
+
|
|
91
|
+
return rounded_samples
|
|
92
|
+
|
|
93
|
+
def register_waveform(
|
|
94
|
+
self,
|
|
95
|
+
name: str,
|
|
96
|
+
waveform: npt.ArrayLike,
|
|
97
|
+
sampling_period_ns: float | None = None,
|
|
98
|
+
):
|
|
99
|
+
if sampling_period_ns is None:
|
|
100
|
+
sampling_period_ns = self._default_sampling_period_ns
|
|
101
|
+
if np.any(np.abs(waveform) > 1):
|
|
102
|
+
raise ValueError("The amplitude must be in the range -1 to 1.")
|
|
103
|
+
self._waveform_library[name] = _Waveform(
|
|
104
|
+
sampling_period_ns=sampling_period_ns,
|
|
105
|
+
iq_array=np.array(waveform, dtype=complex),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def add_event(
|
|
109
|
+
self,
|
|
110
|
+
instrument_alias: str,
|
|
111
|
+
waveform_name: str,
|
|
112
|
+
start_offset_ns: float,
|
|
113
|
+
gain: float = 1.0,
|
|
114
|
+
phase_offset_deg: float = 0.0,
|
|
115
|
+
):
|
|
116
|
+
if waveform_name not in self._waveform_library:
|
|
117
|
+
raise ValueError(f"waveform '{waveform_name}' is not registered.")
|
|
118
|
+
|
|
119
|
+
self._check_and_convert_to_samples(
|
|
120
|
+
instrument_alias, start_offset_ns, "Event start_offset_ns"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
event = _SequencerEvent(
|
|
124
|
+
waveform_name=waveform_name,
|
|
125
|
+
start_offset_ns=start_offset_ns,
|
|
126
|
+
gain=gain,
|
|
127
|
+
phase_offset_deg=phase_offset_deg,
|
|
128
|
+
)
|
|
129
|
+
self._alias_to_events[instrument_alias].append(event)
|
|
130
|
+
|
|
131
|
+
waveform = self._waveform_library[waveform_name]
|
|
132
|
+
end_at_ns = (
|
|
133
|
+
start_offset_ns + len(waveform.iq_array) * waveform.sampling_period_ns
|
|
134
|
+
)
|
|
135
|
+
self._length_ns = max(self._length_ns, end_at_ns)
|
|
136
|
+
|
|
137
|
+
def add_capture_window(
|
|
138
|
+
self,
|
|
139
|
+
instrument_alias: str,
|
|
140
|
+
window_name: str,
|
|
141
|
+
start_offset_ns: float,
|
|
142
|
+
length_ns: float,
|
|
143
|
+
):
|
|
144
|
+
self._check_and_convert_to_samples(
|
|
145
|
+
instrument_alias,
|
|
146
|
+
start_offset_ns,
|
|
147
|
+
f"Capture window '{window_name}' start_offset_ns",
|
|
148
|
+
)
|
|
149
|
+
self._check_and_convert_to_samples(
|
|
150
|
+
instrument_alias, length_ns, f"Capture window '{window_name}' length_ns"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
capwin = _SequencerCaptureWindow(
|
|
154
|
+
name=window_name,
|
|
155
|
+
start_offset_ns=start_offset_ns,
|
|
156
|
+
length_ns=length_ns,
|
|
157
|
+
)
|
|
158
|
+
self._alias_to_capwin[instrument_alias].append(capwin)
|
|
159
|
+
end_at_ns = start_offset_ns + length_ns
|
|
160
|
+
self._length_ns = max(self._length_ns, end_at_ns)
|
|
161
|
+
|
|
162
|
+
def extend_length_ns(self, additional_ns: float):
|
|
163
|
+
self._length_ns += additional_ns
|
|
164
|
+
|
|
165
|
+
def set_iterations(self, iterations: int):
|
|
166
|
+
self._iterations = iterations
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def aligned_length_fs(self) -> int:
|
|
170
|
+
if not self._bindings:
|
|
171
|
+
return math.ceil(self._length_ns * 1e6)
|
|
172
|
+
|
|
173
|
+
lcm_step_fs = 1
|
|
174
|
+
for b in self._bindings.values():
|
|
175
|
+
step_fs = b.sampling_period_fs * b.step_samples
|
|
176
|
+
lcm_step_fs = math.lcm(lcm_step_fs, step_fs)
|
|
177
|
+
|
|
178
|
+
length_fs = math.ceil(self._length_ns * 1e6)
|
|
179
|
+
remainder = length_fs % lcm_step_fs
|
|
180
|
+
if remainder != 0:
|
|
181
|
+
length_fs += lcm_step_fs - remainder
|
|
182
|
+
return length_fs
|
|
183
|
+
|
|
184
|
+
def export_set_fixed_timeline_directive(
|
|
185
|
+
self, instrument_alias: str
|
|
186
|
+
) -> SetFixedTimeline:
|
|
187
|
+
if instrument_alias not in self._bindings:
|
|
188
|
+
raise ValueError(f"Alias '{instrument_alias}' is not bound.")
|
|
189
|
+
|
|
190
|
+
sampling_period_fs = self._bindings[instrument_alias].sampling_period_fs
|
|
191
|
+
|
|
192
|
+
name_to_index: dict[str, int] = {}
|
|
193
|
+
counter = 0
|
|
194
|
+
local_library: list[IqWaveform] = []
|
|
195
|
+
local_events: list[WaveformEvent] = []
|
|
196
|
+
|
|
197
|
+
for event in self._alias_to_events[instrument_alias]:
|
|
198
|
+
if event.waveform_name in name_to_index:
|
|
199
|
+
index = name_to_index[event.waveform_name]
|
|
200
|
+
else:
|
|
201
|
+
waveform = self._waveform_library[event.waveform_name]
|
|
202
|
+
local_library.append(
|
|
203
|
+
IqWaveform(
|
|
204
|
+
sampling_period_fs=round(waveform.sampling_period_ns * 1e6),
|
|
205
|
+
iq_array=waveform.iq_array,
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
index = counter
|
|
209
|
+
name_to_index[event.waveform_name] = index
|
|
210
|
+
counter += 1
|
|
211
|
+
|
|
212
|
+
start_offset_samples = self._check_and_convert_to_samples(
|
|
213
|
+
instrument_alias, event.start_offset_ns, "Export event"
|
|
214
|
+
)
|
|
215
|
+
local_events.append(
|
|
216
|
+
WaveformEvent(
|
|
217
|
+
waveform_index=index,
|
|
218
|
+
start_offset_samples=start_offset_samples,
|
|
219
|
+
gain=event.gain,
|
|
220
|
+
phase_offset_deg=event.phase_offset_deg,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
local_capwins: list[CaptureWindow] = []
|
|
225
|
+
for capwin in self._alias_to_capwin[instrument_alias]:
|
|
226
|
+
local_capwins.append(
|
|
227
|
+
CaptureWindow(
|
|
228
|
+
name=capwin.name,
|
|
229
|
+
start_offset_samples=self._check_and_convert_to_samples(
|
|
230
|
+
instrument_alias, capwin.start_offset_ns, "Export capwin start"
|
|
231
|
+
),
|
|
232
|
+
length_samples=self._check_and_convert_to_samples(
|
|
233
|
+
instrument_alias, capwin.length_ns, "Export capwin length"
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
length_sample = (
|
|
239
|
+
self.aligned_length_fs + sampling_period_fs - 1
|
|
240
|
+
) // sampling_period_fs
|
|
241
|
+
|
|
242
|
+
return SetFixedTimeline(
|
|
243
|
+
waveform_library=local_library,
|
|
244
|
+
events=local_events,
|
|
245
|
+
capture_windows=local_capwins,
|
|
246
|
+
length=length_sample,
|
|
247
|
+
iterations=self._iterations,
|
|
248
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from quelware_core.entities.unit import UnitLabel
|
|
4
|
+
|
|
5
|
+
from quelware_client.core.interfaces.instrument_agent import InstrumentAgent
|
|
6
|
+
from quelware_client.core.interfaces.resource_agent import ResourceAgent
|
|
7
|
+
from quelware_client.core.interfaces.session_agent import SessionAgent
|
|
8
|
+
from quelware_client.core.interfaces.system_configuration_agent import (
|
|
9
|
+
SystemConfigurationAgent,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentContainer:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self._session: SessionAgent | None = None
|
|
18
|
+
self._conf: SystemConfigurationAgent | None = None
|
|
19
|
+
self._rsrc: dict[UnitLabel, ResourceAgent] = {}
|
|
20
|
+
self._inst: dict[UnitLabel, InstrumentAgent] = {}
|
|
21
|
+
|
|
22
|
+
def update_resource_agent(self, unit_label: UnitLabel, agent: ResourceAgent | None):
|
|
23
|
+
if agent is None:
|
|
24
|
+
if unit_label in self._rsrc:
|
|
25
|
+
self._rsrc[unit_label]
|
|
26
|
+
else:
|
|
27
|
+
logger.warning(f"ResourceAgent for '{unit_label}' not found. Continue.")
|
|
28
|
+
else:
|
|
29
|
+
self._rsrc[unit_label] = agent
|
|
30
|
+
|
|
31
|
+
def update_instrument_agent(
|
|
32
|
+
self, unit_label: UnitLabel, agent: InstrumentAgent | None
|
|
33
|
+
):
|
|
34
|
+
if agent is None:
|
|
35
|
+
if unit_label in self._rsrc:
|
|
36
|
+
self._inst[unit_label]
|
|
37
|
+
else:
|
|
38
|
+
logger.warning(
|
|
39
|
+
f"InstrumentAgent for '{unit_label}' not found. Continue."
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
self._inst[unit_label] = agent
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def session(self) -> SessionAgent:
|
|
46
|
+
if self._session is None:
|
|
47
|
+
raise ValueError("SessionAgent not set.")
|
|
48
|
+
return self._session
|
|
49
|
+
|
|
50
|
+
@session.setter
|
|
51
|
+
def session(self, val: SessionAgent):
|
|
52
|
+
self._session = val
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def system_configuration(self) -> SystemConfigurationAgent:
|
|
56
|
+
if self._conf is None:
|
|
57
|
+
raise ValueError("ConfigurationAgent not set.")
|
|
58
|
+
return self._conf
|
|
59
|
+
|
|
60
|
+
@system_configuration.setter
|
|
61
|
+
def system_configuration(self, val: SystemConfigurationAgent):
|
|
62
|
+
self._conf = val
|
|
63
|
+
|
|
64
|
+
def resource(self, unit_label: UnitLabel) -> ResourceAgent:
|
|
65
|
+
if unit_label not in self._rsrc:
|
|
66
|
+
raise ValueError(f"ResouceAgent for '{unit_label}' not set.")
|
|
67
|
+
return self._rsrc[unit_label]
|
|
68
|
+
|
|
69
|
+
def instrument(self, unit_label: UnitLabel) -> InstrumentAgent:
|
|
70
|
+
if unit_label not in self._inst:
|
|
71
|
+
raise ValueError(f"InstrumentAgent for '{unit_label}' not set.")
|
|
72
|
+
return self._inst[unit_label]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Callable, Collection
|
|
3
|
+
from typing import TypeAlias, TypeVar, cast
|
|
4
|
+
|
|
5
|
+
from quelware_core.entities.instrument import InstrumentInfo
|
|
6
|
+
from quelware_core.entities.port import PortInfo
|
|
7
|
+
from quelware_core.entities.resource import (
|
|
8
|
+
ResourceId,
|
|
9
|
+
ResourceInfo,
|
|
10
|
+
extract_unit_label,
|
|
11
|
+
)
|
|
12
|
+
from quelware_core.entities.unit import UnitLabel
|
|
13
|
+
|
|
14
|
+
from quelware_client.core._agent_container import AgentContainer
|
|
15
|
+
from quelware_client.core.interfaces.instrument_agent import InstrumentAgent
|
|
16
|
+
|
|
17
|
+
from ._session import Session
|
|
18
|
+
from .interfaces.resource_agent import ResourceAgent
|
|
19
|
+
|
|
20
|
+
A = TypeVar("A")
|
|
21
|
+
AgentFactory: TypeAlias = Callable[[UnitLabel], A]
|
|
22
|
+
_ResourceId: TypeAlias = ResourceId | str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class QuelwareClient:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
agent: AgentContainer,
|
|
29
|
+
resource_agent_factory: AgentFactory[ResourceAgent] | None = None,
|
|
30
|
+
instrument_agent_factory: AgentFactory[InstrumentAgent] | None = None,
|
|
31
|
+
close_handlers: list[Callable[[], None]] | None = None,
|
|
32
|
+
):
|
|
33
|
+
self._agent = agent
|
|
34
|
+
self._rsrc_agent_factory = resource_agent_factory
|
|
35
|
+
self._inst_agent_factory = instrument_agent_factory
|
|
36
|
+
self._unit_labels: list[UnitLabel] = []
|
|
37
|
+
self._close_handlers = close_handlers or []
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def agent(self) -> AgentContainer:
|
|
41
|
+
return self._agent
|
|
42
|
+
|
|
43
|
+
async def start(self):
|
|
44
|
+
await self.initialize()
|
|
45
|
+
|
|
46
|
+
async def stop(self):
|
|
47
|
+
for handler in self._close_handlers:
|
|
48
|
+
handler()
|
|
49
|
+
|
|
50
|
+
async def __aenter__(self):
|
|
51
|
+
await self.start()
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
55
|
+
await self.stop()
|
|
56
|
+
|
|
57
|
+
async def initialize(self):
|
|
58
|
+
self._unit_labels = await self._agent.system_configuration.list_units()
|
|
59
|
+
for ul in self._unit_labels:
|
|
60
|
+
if self._rsrc_agent_factory:
|
|
61
|
+
self._agent.update_resource_agent(ul, self._rsrc_agent_factory(ul))
|
|
62
|
+
if self._inst_agent_factory:
|
|
63
|
+
self._agent.update_instrument_agent(ul, self._inst_agent_factory(ul))
|
|
64
|
+
|
|
65
|
+
def list_unit_labels(self) -> list[UnitLabel]:
|
|
66
|
+
return list(self._unit_labels)
|
|
67
|
+
|
|
68
|
+
def create_session(
|
|
69
|
+
self,
|
|
70
|
+
resource_ids: Collection[_ResourceId],
|
|
71
|
+
ttl_ms: int = 4000,
|
|
72
|
+
tentative_ttl_ms: int = 1000,
|
|
73
|
+
) -> Session:
|
|
74
|
+
return Session(
|
|
75
|
+
resource_ids=cast(list[ResourceId], resource_ids),
|
|
76
|
+
agent=self._agent,
|
|
77
|
+
ttl_ms=ttl_ms,
|
|
78
|
+
tentative_ttl_ms=tentative_ttl_ms,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
async def list_resource_infos(self) -> list[ResourceInfo]:
|
|
82
|
+
coros = [
|
|
83
|
+
self._agent.resource(ul).list_resource_infos() for ul in self._unit_labels
|
|
84
|
+
]
|
|
85
|
+
results = await asyncio.gather(*coros)
|
|
86
|
+
rsrc_infos: list[ResourceInfo] = []
|
|
87
|
+
for res in results:
|
|
88
|
+
rsrc_infos.extend(res)
|
|
89
|
+
return rsrc_infos
|
|
90
|
+
|
|
91
|
+
async def get_port_info(self, port_id: ResourceId) -> PortInfo:
|
|
92
|
+
unit_label = extract_unit_label(port_id)
|
|
93
|
+
port = await self._agent.resource(unit_label).get_port_info(port_id)
|
|
94
|
+
return port
|
|
95
|
+
|
|
96
|
+
async def get_instrument_info(self, instrument_id: ResourceId) -> InstrumentInfo:
|
|
97
|
+
unit_label = extract_unit_label(instrument_id)
|
|
98
|
+
inst = await self._agent.resource(unit_label).get_instrument_info(instrument_id)
|
|
99
|
+
return inst
|