quelware-client 0.7.2__tar.gz → 0.7.3__tar.gz
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-0.7.2 → quelware_client-0.7.3}/CHANGELOG.md +15 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/PKG-INFO +1 -1
- quelware_client-0.7.3/examples/fixed_timeline.py +173 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/pyproject.toml +1 -1
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/_session.py +189 -19
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/exceptions/__init__.py +35 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/instrument_driver/__init__.py +35 -4
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/interfaces/instrument_agent/__init__.py +1 -1
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/infra/instrument_agent_grpc/__init__.py +2 -1
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/testing/instrument_agent_mock/__init__.py +1 -1
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/core/instrument_driver/test_instrument_driver.py +8 -0
- quelware_client-0.7.3/tests/core/session/test_not_triggered.py +286 -0
- quelware_client-0.7.3/tests/core/session/test_unit_busy.py +218 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/core/session/test_wait_for_results.py +18 -6
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/infra/instrument_agent_grpc/test_retry_on_connection_lost.py +1 -1
- {quelware_client-0.7.2 → quelware_client-0.7.3}/uv.lock +1 -1
- {quelware_client-0.7.2 → quelware_client-0.7.3}/.gitignore +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/LICENSE +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/Makefile +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/README.md +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/client/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/client/_grpc.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/client/_standalone_grpc.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/client/helpers/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/client/helpers/instrument_resolver/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/client/helpers/sequencer/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/_agent_container.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/_client.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/_utils.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/interfaces/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/interfaces/diagnostics_agent/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/interfaces/health_agent/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/interfaces/pat_provider/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/interfaces/resource_agent/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/interfaces/session_agent/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/interfaces/system_configuration_agent/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/interfaces/trigger_agent/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/interfaces/worker_agent/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/trigger_count_proposer/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/unit_control/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/infra/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/infra/_grpc_retry.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/infra/_timeouts.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/infra/diagnostics_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/infra/health_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/infra/pat_provider_file/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/infra/resource_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/infra/session_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/infra/system_configuration_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/infra/trigger_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/infra/worker_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/py.typed +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/testing/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/testing/mock_client.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/testing/resource_agent_mock/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/testing/session_agent_mock/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/testing/system_configuration_agent_mock/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/testing/trigger_agent_mock/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/testing/worker_agent_mock/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/core/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/core/client/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/core/client/helpers/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/core/client/helpers/test_instrument_resolver.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/core/client/helpers/test_sequencer.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/core/client/test_client.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/core/instrument_driver/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/core/session/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/core/session/test_session.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/core/session/test_trigger.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/infra/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/infra/health_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/infra/health_agent_grpc/test_health_check.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/infra/instrument_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.3}/tests/infra/instrument_agent_grpc/test_wait_for_result.py +0 -0
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.7.3] - 2026-10-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `examples/fixed_timeline.py`, the code of the fixed-timeline tutorial as a script you can run against a system. The docs quote it, and the lint and the type check cover it, so it keeps up with the API.
|
|
10
|
+
- `NotTriggeredError`: `Session.wait_for_results`, `Session.wait_for_result`, `Session.fetch_result` and the same methods of `InstrumentDriver` raise it immediately if an instrument was not in the last trigger on its unit. Before, the request went to the unit, which could hang (armed but not triggered), return an empty result, or fail with `INTERNAL`. Within a session, each unit has at most one run: a new trigger on the unit replaces it, and initializing an instrument on the unit clears it. Other sessions on the unit have their own runs.
|
|
11
|
+
- `UnitBusyError`: `Session.trigger` and `Session.initialize` raise it immediately if a trigger or initialize of the same session is already running on the unit.
|
|
12
|
+
- `InitializeFailedError`: `Session.initialize` raises it, once every unit has finished, when some units failed; `failures` holds the error of each, by unit.
|
|
13
|
+
- `Session.initialize(instrument_ids, parallel=True)`, which initializes the units at the same time, or one after another when not `parallel`, and every unit also after one fails, `Session.fetch_result(instrument_id)` and `Session.wait_for_result(instrument_id, timeout_sec=None)`. The `InstrumentDriver` methods of the same names now call them, so the session knows which instruments can be waited for.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `InstrumentDriver` requires the `session` it belongs to. `create_instrument_driver_fixed_timeline` passes it, so only code that builds a driver directly needs a change.
|
|
18
|
+
- `InstrumentAgent.apply` is renamed `arm`, because it arms the instruments (`InstrumentDriver.apply` configures an instrument). It still calls the `Apply` RPC.
|
|
19
|
+
|
|
5
20
|
## [0.7.2] - 2026-09-29
|
|
6
21
|
|
|
7
22
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: quelware-client
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Project-URL: Repository, https://github.com/quel-inc/quelware-client
|
|
5
5
|
Project-URL: Documentation, https://quel-inc.github.io/quelware-client/
|
|
6
6
|
Project-URL: Changelog, https://github.com/quel-inc/quelware-client/blob/main/quelware-client/CHANGELOG.md
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""A drive pulse and a readout on fixed-timeline instruments.
|
|
2
|
+
|
|
3
|
+
The code of the fixed-timeline tutorial, which the docs quote from here. Run it
|
|
4
|
+
against your system:
|
|
5
|
+
|
|
6
|
+
python fixed_timeline.py <host> --unit <label>
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# --8<-- [start:imports]
|
|
10
|
+
import argparse
|
|
11
|
+
import asyncio
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from quelware_core.entities import directives
|
|
15
|
+
from quelware_core.entities.instrument import (
|
|
16
|
+
FixedTimelineProfile,
|
|
17
|
+
InstrumentDefinition,
|
|
18
|
+
InstrumentInfo,
|
|
19
|
+
InstrumentMode,
|
|
20
|
+
InstrumentRole,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from quelware_client import create_quelware_client
|
|
24
|
+
from quelware_client.client.helpers.sequencer import Sequencer
|
|
25
|
+
from quelware_client.core import QuelwareClient, Session
|
|
26
|
+
from quelware_client.core.exceptions import RunFailedError
|
|
27
|
+
from quelware_client.core.instrument_driver import (
|
|
28
|
+
create_instrument_driver_fixed_timeline,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# --8<-- [end:imports]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# --8<-- [start:deploy]
|
|
35
|
+
def definition(alias: str, role: InstrumentRole, center_hz: float):
|
|
36
|
+
return InstrumentDefinition(
|
|
37
|
+
alias=alias,
|
|
38
|
+
mode=InstrumentMode.FIXED_TIMELINE,
|
|
39
|
+
role=role,
|
|
40
|
+
profile=FixedTimelineProfile(
|
|
41
|
+
frequency_range_min=center_hz - 2.5e6,
|
|
42
|
+
frequency_range_max=center_hz + 2.5e6,
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def deploy(
|
|
48
|
+
qc: QuelwareClient, drive_port: str, readout_port: str
|
|
49
|
+
) -> tuple[InstrumentInfo, InstrumentInfo]:
|
|
50
|
+
async with qc.create_session([drive_port, readout_port]) as session:
|
|
51
|
+
(drive,) = await session.deploy_instruments(
|
|
52
|
+
drive_port, [definition("drive", InstrumentRole.TRANSMITTER, 5.0e9)]
|
|
53
|
+
)
|
|
54
|
+
(readout,) = await session.deploy_instruments(
|
|
55
|
+
readout_port,
|
|
56
|
+
[definition("readout", InstrumentRole.TRANSCEIVER_LOOPBACK, 6.0e9)],
|
|
57
|
+
)
|
|
58
|
+
return drive, readout
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --8<-- [end:deploy]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# --8<-- [start:timeline]
|
|
65
|
+
def build_timeline(
|
|
66
|
+
inst_info: InstrumentInfo, pulse_start_ns: float, capture: bool
|
|
67
|
+
) -> Sequencer:
|
|
68
|
+
alias = inst_info.definition.alias
|
|
69
|
+
sampling_period_ns = inst_info.config.sampling_period_fs * 1e-6
|
|
70
|
+
|
|
71
|
+
seq = Sequencer(default_sampling_period_ns=sampling_period_ns)
|
|
72
|
+
seq.bind(
|
|
73
|
+
alias,
|
|
74
|
+
sampling_period_fs=inst_info.config.sampling_period_fs,
|
|
75
|
+
step_samples=inst_info.config.timeline_step_samples,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
n = int(200.0 / sampling_period_ns) # a 200 ns rectangular pulse
|
|
79
|
+
seq.register_waveform("pulse", np.ones(n, dtype=complex))
|
|
80
|
+
seq.add_event(alias, "pulse", start_offset_ns=pulse_start_ns)
|
|
81
|
+
if capture:
|
|
82
|
+
seq.add_capture_window(
|
|
83
|
+
alias, "capture", start_offset_ns=pulse_start_ns, length_ns=1000.0
|
|
84
|
+
)
|
|
85
|
+
seq.extend_length_ns(100_000.0) # gap before the next shot
|
|
86
|
+
seq.set_iterations(1000) # average over 1000 shots
|
|
87
|
+
return seq
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# --8<-- [end:timeline]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --8<-- [start:trigger]
|
|
94
|
+
async def configure(
|
|
95
|
+
session: Session, inst_info: InstrumentInfo, hz: float, seq: Sequencer
|
|
96
|
+
) -> None:
|
|
97
|
+
driver = create_instrument_driver_fixed_timeline(session, inst_info)
|
|
98
|
+
alias = inst_info.definition.alias
|
|
99
|
+
settings: list[directives.FixedTimelineDirective] = [directives.SetFrequency(hz=hz)]
|
|
100
|
+
if inst_info.definition.role is not InstrumentRole.TRANSMITTER:
|
|
101
|
+
settings.append(
|
|
102
|
+
directives.SetCaptureMode(mode=directives.CaptureMode.AVERAGED_WAVEFORM)
|
|
103
|
+
)
|
|
104
|
+
settings.append(seq.export_set_fixed_timeline_directive(alias))
|
|
105
|
+
await driver.initialize()
|
|
106
|
+
await driver.apply(settings)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def trigger(
|
|
110
|
+
session: Session, drive: InstrumentInfo, readout: InstrumentInfo
|
|
111
|
+
) -> None:
|
|
112
|
+
await configure(session, drive, 5.0e9, build_timeline(drive, 0.0, capture=False))
|
|
113
|
+
await configure(
|
|
114
|
+
session, readout, 6.0e9, build_timeline(readout, 300.0, capture=True)
|
|
115
|
+
)
|
|
116
|
+
await session.trigger([drive.id, readout.id])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --8<-- [end:trigger]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --8<-- [start:wait]
|
|
123
|
+
async def wait(
|
|
124
|
+
session: Session, drive: InstrumentInfo, readout: InstrumentInfo
|
|
125
|
+
) -> np.ndarray:
|
|
126
|
+
try:
|
|
127
|
+
results = await session.wait_for_results(
|
|
128
|
+
[drive.id, readout.id], timeout_sec=10.0
|
|
129
|
+
)
|
|
130
|
+
except RunFailedError as e:
|
|
131
|
+
for instrument_id, error in e.failures.items():
|
|
132
|
+
print(f"{instrument_id} failed: {error}")
|
|
133
|
+
raise
|
|
134
|
+
return results[readout.id].iq_waveform_result["capture"][0].iq_array
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# --8<-- [end:wait]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --8<-- [start:main]
|
|
141
|
+
async def main(host: str, port: int, drive_port: str, readout_port: str) -> None:
|
|
142
|
+
qc = create_quelware_client(host, port)
|
|
143
|
+
async with qc:
|
|
144
|
+
drive, readout = await deploy(qc, drive_port, readout_port)
|
|
145
|
+
async with qc.create_session([drive.id, readout.id], ttl_ms=10_000) as session:
|
|
146
|
+
await trigger(session, drive, readout)
|
|
147
|
+
iq = await wait(session, drive, readout)
|
|
148
|
+
print(f"captured {len(iq)} samples")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# --8<-- [end:main]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
parser = argparse.ArgumentParser(
|
|
156
|
+
description="Run a drive pulse and a readout on fixed-timeline instruments."
|
|
157
|
+
)
|
|
158
|
+
parser.add_argument("host", help="server host")
|
|
159
|
+
parser.add_argument("--port", type=int, default=50051, help="server port")
|
|
160
|
+
parser.add_argument("--unit", required=True, help="unit label")
|
|
161
|
+
parser.add_argument("--drive-port", default="tx_p02", help="a transmitter port")
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--readout-port", default="trx_p00p01", help="a transceiver port"
|
|
164
|
+
)
|
|
165
|
+
args = parser.parse_args()
|
|
166
|
+
asyncio.run(
|
|
167
|
+
main(
|
|
168
|
+
args.host,
|
|
169
|
+
args.port,
|
|
170
|
+
f"{args.unit}:{args.drive_port}",
|
|
171
|
+
f"{args.unit}:{args.readout_port}",
|
|
172
|
+
)
|
|
173
|
+
)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
3
|
import math
|
|
4
|
-
from collections.abc import Collection, Mapping
|
|
4
|
+
from collections.abc import Collection, Iterator, Mapping
|
|
5
|
+
from contextlib import contextmanager
|
|
5
6
|
from types import TracebackType
|
|
6
7
|
from typing import cast
|
|
7
8
|
|
|
@@ -15,7 +16,13 @@ from quelware_core.entities.session import SessionToken
|
|
|
15
16
|
from quelware_core.entities.unit import UnitLabel
|
|
16
17
|
|
|
17
18
|
from quelware_client.core import AgentContainer
|
|
18
|
-
from quelware_client.core.exceptions import
|
|
19
|
+
from quelware_client.core.exceptions import (
|
|
20
|
+
InitializeFailedError,
|
|
21
|
+
NotTriggeredError,
|
|
22
|
+
RunFailedError,
|
|
23
|
+
ServiceUnavailableError,
|
|
24
|
+
UnitBusyError,
|
|
25
|
+
)
|
|
19
26
|
from quelware_client.core.trigger_count_proposer import (
|
|
20
27
|
FixedOffsetTriggerCountProposer,
|
|
21
28
|
TriggerCountProposer,
|
|
@@ -45,6 +52,13 @@ class Session:
|
|
|
45
52
|
await session.trigger(instrument_ids)
|
|
46
53
|
results = await session.wait_for_results(instrument_ids)
|
|
47
54
|
```
|
|
55
|
+
|
|
56
|
+
Within a session, each unit has at most one run. A new trigger on a unit
|
|
57
|
+
replaces its run, and initializing an instrument on the unit clears it.
|
|
58
|
+
Other sessions on the same unit have their own runs. The session remembers
|
|
59
|
+
which instruments were in the last trigger on each unit, and waits only for
|
|
60
|
+
those. Another `Session` object built with the same token does not share
|
|
61
|
+
this record.
|
|
48
62
|
"""
|
|
49
63
|
|
|
50
64
|
def __init__( # noqa: PLR0913
|
|
@@ -90,6 +104,10 @@ class Session:
|
|
|
90
104
|
self._unit_to_ids.setdefault(ul, []).append(rid)
|
|
91
105
|
|
|
92
106
|
self._check_lock = not skip_lock_check
|
|
107
|
+
# instruments in the last trigger on each unit of this session
|
|
108
|
+
self._triggered: set[ResourceId] = set()
|
|
109
|
+
# units where a trigger or initialize of this session is running now
|
|
110
|
+
self._busy_units: set[UnitLabel] = set()
|
|
93
111
|
|
|
94
112
|
async def open(self):
|
|
95
113
|
"""Open the session, locking its resources and obtaining a token.
|
|
@@ -262,33 +280,53 @@ class Session:
|
|
|
262
280
|
instrument_ids: Collection[ResourceId],
|
|
263
281
|
wait_ms: int | None = None,
|
|
264
282
|
) -> int:
|
|
265
|
-
"""
|
|
283
|
+
"""Arm the given instruments, then trigger them.
|
|
266
284
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
285
|
+
Arming puts the configuration of each instrument on its unit's device,
|
|
286
|
+
as the run of the session there. Then the server schedules a trigger,
|
|
287
|
+
at which every unit starts. If the server's trigger service is
|
|
288
|
+
unavailable, the client triggers the units itself: a self-timed trigger
|
|
289
|
+
for a single unit, or a clock-synchronized trigger across several.
|
|
271
290
|
|
|
272
291
|
Args:
|
|
273
292
|
instrument_ids: Instruments to trigger.
|
|
274
|
-
wait_ms: Delay, in milliseconds, from now to when the
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
293
|
+
wait_ms: Delay, in milliseconds, from now to when the run starts,
|
|
294
|
+
giving every unit time to start on it. ``None`` leaves it to
|
|
295
|
+
the server, which also raises a wait shorter than it allows.
|
|
296
|
+
When the client triggers the units itself, it waits at least
|
|
297
|
+
500 ms.
|
|
279
298
|
|
|
280
299
|
Returns:
|
|
281
300
|
The clock count at which the trigger was scheduled.
|
|
301
|
+
|
|
302
|
+
Raises:
|
|
303
|
+
UnitBusyError: Immediately, if a trigger or initialize of this
|
|
304
|
+
session is already running on one of the units.
|
|
282
305
|
"""
|
|
283
306
|
unit_to_ids = create_unit_to_ids_map(instrument_ids)
|
|
307
|
+
with self._claiming(unit_to_ids):
|
|
308
|
+
# arming replaces the session's run on the unit, so forget the old
|
|
309
|
+
# one now, even if this trigger fails
|
|
310
|
+
self._forget_runs_on(unit_to_ids)
|
|
311
|
+
scheduled = await self._arm_and_trigger(
|
|
312
|
+
instrument_ids, unit_to_ids, wait_ms
|
|
313
|
+
)
|
|
314
|
+
self._triggered.update(instrument_ids)
|
|
315
|
+
return scheduled
|
|
284
316
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
317
|
+
async def _arm_and_trigger(
|
|
318
|
+
self,
|
|
319
|
+
instrument_ids: Collection[ResourceId],
|
|
320
|
+
unit_to_ids: dict[UnitLabel, list[ResourceId]],
|
|
321
|
+
wait_ms: int | None,
|
|
322
|
+
) -> int:
|
|
323
|
+
logger.info(f"starting arming (token= {self.token} )")
|
|
324
|
+
arm_coros = [
|
|
325
|
+
self._agent.instrument(unit_label).arm(self.token, ids)
|
|
288
326
|
for unit_label, ids in unit_to_ids.items()
|
|
289
327
|
]
|
|
290
|
-
await asyncio.gather(*
|
|
291
|
-
logger.info(f"finished
|
|
328
|
+
await asyncio.gather(*arm_coros)
|
|
329
|
+
logger.info(f"finished arming (token= {self.token} )")
|
|
292
330
|
|
|
293
331
|
try:
|
|
294
332
|
scheduled = await self._agent.trigger.trigger(
|
|
@@ -314,8 +352,10 @@ class Session:
|
|
|
314
352
|
) -> dict[ResourceId, ResultContainer]:
|
|
315
353
|
"""Wait for the result of every one of the instruments of a run.
|
|
316
354
|
|
|
317
|
-
Pass the instruments given to `trigger()
|
|
318
|
-
capture window is waited for
|
|
355
|
+
Pass the instruments given to `trigger()`. An instrument without a
|
|
356
|
+
capture window is waited for too, and its result is empty. Only the
|
|
357
|
+
instruments in the last trigger on each unit can be waited for, and not
|
|
358
|
+
after an instrument on that unit is initialized.
|
|
319
359
|
|
|
320
360
|
Args:
|
|
321
361
|
instrument_ids: Instruments to wait for.
|
|
@@ -325,11 +365,14 @@ class Session:
|
|
|
325
365
|
Each instrument's result.
|
|
326
366
|
|
|
327
367
|
Raises:
|
|
368
|
+
NotTriggeredError: Immediately, if an instrument was not in the last
|
|
369
|
+
trigger on its unit.
|
|
328
370
|
RunFailedError: If any instrument failed, once all of them have
|
|
329
371
|
finished, with the error of each that failed and the results of
|
|
330
372
|
the others.
|
|
331
373
|
TimeoutError: If some are still running when ``timeout_sec`` runs out.
|
|
332
374
|
"""
|
|
375
|
+
self._ensure_triggered(instrument_ids)
|
|
333
376
|
tasks = {
|
|
334
377
|
asyncio.ensure_future(
|
|
335
378
|
self._agent.instrument(extract_unit_label(rid)).wait_for_result(
|
|
@@ -361,6 +404,133 @@ class Session:
|
|
|
361
404
|
raise RunFailedError(failures, results)
|
|
362
405
|
return results
|
|
363
406
|
|
|
407
|
+
async def initialize(
|
|
408
|
+
self, instrument_ids: Collection[ResourceId], parallel: bool = True
|
|
409
|
+
) -> None:
|
|
410
|
+
"""Initialize the given instruments.
|
|
411
|
+
|
|
412
|
+
An initialized instrument is empty: armed, it plays and captures
|
|
413
|
+
nothing until it is given a timeline again. This also clears the
|
|
414
|
+
session's run on each of their units: no instrument on those units can
|
|
415
|
+
be waited for until the next trigger there.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
instrument_ids: Instruments to initialize.
|
|
419
|
+
parallel: Initialize the units at the same time, the default; when
|
|
420
|
+
False, one unit after another. Either way every unit is
|
|
421
|
+
initialized, also after one fails, and the instruments of one
|
|
422
|
+
unit are always initialized in one call.
|
|
423
|
+
|
|
424
|
+
Raises:
|
|
425
|
+
UnitBusyError: Immediately, if a trigger or initialize of this
|
|
426
|
+
session is already running on one of the units.
|
|
427
|
+
InitializeFailedError: If some units failed, once every unit has
|
|
428
|
+
finished, with the error of each unit that failed.
|
|
429
|
+
"""
|
|
430
|
+
unit_to_ids = create_unit_to_ids_map(instrument_ids)
|
|
431
|
+
with self._claiming(unit_to_ids):
|
|
432
|
+
self._forget_runs_on(unit_to_ids)
|
|
433
|
+
if parallel:
|
|
434
|
+
# wait for every unit, so that none is still initializing once
|
|
435
|
+
# the units are let go
|
|
436
|
+
outcomes = await asyncio.gather(
|
|
437
|
+
*(
|
|
438
|
+
self._initialize_unit(unit_label, ids)
|
|
439
|
+
for unit_label, ids in unit_to_ids.items()
|
|
440
|
+
)
|
|
441
|
+
)
|
|
442
|
+
else:
|
|
443
|
+
outcomes = [
|
|
444
|
+
await self._initialize_unit(unit_label, ids)
|
|
445
|
+
for unit_label, ids in unit_to_ids.items()
|
|
446
|
+
]
|
|
447
|
+
failures = {
|
|
448
|
+
unit_label: outcome
|
|
449
|
+
for unit_label, outcome in zip(unit_to_ids, outcomes, strict=True)
|
|
450
|
+
if outcome is not None
|
|
451
|
+
}
|
|
452
|
+
if failures:
|
|
453
|
+
raise InitializeFailedError(failures, len(unit_to_ids))
|
|
454
|
+
|
|
455
|
+
async def _initialize_unit(
|
|
456
|
+
self, unit_label: UnitLabel, instrument_ids: list[ResourceId]
|
|
457
|
+
) -> Exception | None:
|
|
458
|
+
"""Initialize the instruments of a unit, and return its error, if any."""
|
|
459
|
+
try:
|
|
460
|
+
await self._agent.instrument(unit_label).initialize(
|
|
461
|
+
self.token, instrument_ids
|
|
462
|
+
)
|
|
463
|
+
except Exception as e:
|
|
464
|
+
return e
|
|
465
|
+
return None
|
|
466
|
+
|
|
467
|
+
async def fetch_result(self, instrument_id: ResourceId) -> ResultContainer:
|
|
468
|
+
"""Fetch the result of one instrument, with a single request.
|
|
469
|
+
|
|
470
|
+
Unlike `wait_for_result()`, this does not retry when the request times
|
|
471
|
+
out.
|
|
472
|
+
|
|
473
|
+
Raises:
|
|
474
|
+
NotTriggeredError: Immediately, if the instrument was not in the last
|
|
475
|
+
trigger on its unit.
|
|
476
|
+
"""
|
|
477
|
+
self._ensure_triggered([instrument_id])
|
|
478
|
+
agent = self._agent.instrument(extract_unit_label(instrument_id))
|
|
479
|
+
return await agent.fetch_result(self.token, instrument_id)
|
|
480
|
+
|
|
481
|
+
async def wait_for_result(
|
|
482
|
+
self, instrument_id: ResourceId, timeout_sec: float | None = None
|
|
483
|
+
) -> ResultContainer:
|
|
484
|
+
"""Wait for the result of one instrument.
|
|
485
|
+
|
|
486
|
+
Unlike `wait_for_results()`, this raises the instrument's own error if
|
|
487
|
+
it failed.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
instrument_id: Instrument to wait for.
|
|
491
|
+
timeout_sec: Maximum time to wait; ``None`` waits without limit.
|
|
492
|
+
|
|
493
|
+
Raises:
|
|
494
|
+
NotTriggeredError: Immediately, if the instrument was not in the last
|
|
495
|
+
trigger on its unit.
|
|
496
|
+
"""
|
|
497
|
+
self._ensure_triggered([instrument_id])
|
|
498
|
+
agent = self._agent.instrument(extract_unit_label(instrument_id))
|
|
499
|
+
return await agent.wait_for_result(self.token, instrument_id, timeout_sec)
|
|
500
|
+
|
|
501
|
+
@contextmanager
|
|
502
|
+
def _claiming(self, unit_labels: Collection[UnitLabel]) -> Iterator[None]:
|
|
503
|
+
"""Mark the units as busy during a trigger or initialize.
|
|
504
|
+
|
|
505
|
+
Two of them at the same time on one unit would make the record of
|
|
506
|
+
triggered instruments wrong.
|
|
507
|
+
"""
|
|
508
|
+
units = set(unit_labels)
|
|
509
|
+
if busy := sorted(units & self._busy_units):
|
|
510
|
+
raise UnitBusyError().with_unit_labels(busy)
|
|
511
|
+
self._busy_units |= units
|
|
512
|
+
try:
|
|
513
|
+
yield
|
|
514
|
+
finally:
|
|
515
|
+
self._busy_units -= units
|
|
516
|
+
|
|
517
|
+
def _forget_runs_on(self, unit_labels: Collection[UnitLabel]) -> None:
|
|
518
|
+
"""Forget the triggered instruments on the units."""
|
|
519
|
+
units = set(unit_labels)
|
|
520
|
+
self._triggered = {
|
|
521
|
+
rid for rid in self._triggered if extract_unit_label(rid) not in units
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
def _ensure_triggered(self, instrument_ids: Collection[ResourceId]) -> None:
|
|
525
|
+
"""Raise if an instrument was not in the last trigger on its unit.
|
|
526
|
+
|
|
527
|
+
Raises:
|
|
528
|
+
NotTriggeredError: With the ids of those instruments.
|
|
529
|
+
"""
|
|
530
|
+
missing = sorted(set(instrument_ids) - self._triggered)
|
|
531
|
+
if missing:
|
|
532
|
+
raise NotTriggeredError().with_resource_ids(missing)
|
|
533
|
+
|
|
364
534
|
async def _client_side_trigger_fallback(
|
|
365
535
|
self,
|
|
366
536
|
unit_to_ids: dict[UnitLabel, list[ResourceId]],
|
{quelware_client-0.7.2 → quelware_client-0.7.3}/src/quelware_client/core/exceptions/__init__.py
RENAMED
|
@@ -101,3 +101,38 @@ class RunFailedError(QuelwareClientError):
|
|
|
101
101
|
f"{len(self.failures)} of {len(self.failures) + len(self.results)} "
|
|
102
102
|
f"instruments failed: {detail}"
|
|
103
103
|
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class InitializeFailedError(QuelwareClientError):
|
|
107
|
+
"""Raised when the initialize of some units failed.
|
|
108
|
+
|
|
109
|
+
``failures`` holds the error of each unit that failed, by its label. The
|
|
110
|
+
other units were initialized.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(self, failures: Mapping[UnitLabel, BaseException], units: int):
|
|
114
|
+
self.failures = dict(failures)
|
|
115
|
+
detail = "; ".join(f"{u}: {e}" for u, e in sorted(self.failures.items()))
|
|
116
|
+
super().__init__(
|
|
117
|
+
f"initialize failed on {len(self.failures)} of {units} units: {detail}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class NotTriggeredError(_ErrorWithResourceIdsMixin, QuelwareClientError):
|
|
122
|
+
"""Raised when waiting for an instrument that was not triggered.
|
|
123
|
+
|
|
124
|
+
Within a session, each unit has at most one run. A new trigger on the unit
|
|
125
|
+
replaces it, and initializing an instrument on the unit clears it. So only
|
|
126
|
+
the instruments in the last trigger on each unit can be waited for.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
DEFAULT_MESSAGE = "The instruments were not in the last trigger on their unit."
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class UnitBusyError(_ErrorWithUnitLabelsMixin, QuelwareClientError):
|
|
133
|
+
"""Raised when a trigger or initialize of the same session is already
|
|
134
|
+
running on the unit."""
|
|
135
|
+
|
|
136
|
+
DEFAULT_MESSAGE = (
|
|
137
|
+
"A trigger or initialize of this session is already running on the unit."
|
|
138
|
+
)
|
|
@@ -29,6 +29,12 @@ P = TypeVar("P", bound="ProfileVariant")
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class InstrumentDriver(Generic[D, C, P]):
|
|
32
|
+
"""Configure one deployed instrument, within the session it belongs to.
|
|
33
|
+
|
|
34
|
+
Build one with `create_instrument_driver_fixed_timeline()`. Its settings
|
|
35
|
+
take effect when the session triggers it; see `Session.trigger()`.
|
|
36
|
+
"""
|
|
37
|
+
|
|
32
38
|
def __init__( # noqa: PLR0913
|
|
33
39
|
self,
|
|
34
40
|
session_token: SessionToken,
|
|
@@ -37,6 +43,7 @@ class InstrumentDriver(Generic[D, C, P]):
|
|
|
37
43
|
definition: InstrumentDefinition[P],
|
|
38
44
|
config: C,
|
|
39
45
|
instrument_agent: InstrumentAgent,
|
|
46
|
+
session: Session,
|
|
40
47
|
):
|
|
41
48
|
self._token = session_token
|
|
42
49
|
self._id = instrument_id
|
|
@@ -44,6 +51,8 @@ class InstrumentDriver(Generic[D, C, P]):
|
|
|
44
51
|
self._definition = definition
|
|
45
52
|
self._config = config
|
|
46
53
|
self._agent = instrument_agent
|
|
54
|
+
# the session knows which instruments can be waited for
|
|
55
|
+
self._session = session
|
|
47
56
|
|
|
48
57
|
@overload
|
|
49
58
|
async def apply(self, directive: D) -> bool: ...
|
|
@@ -52,21 +61,30 @@ class InstrumentDriver(Generic[D, C, P]):
|
|
|
52
61
|
async def apply(self, directive: Sequence[D]) -> bool: ...
|
|
53
62
|
|
|
54
63
|
async def apply(self, directive) -> bool:
|
|
64
|
+
"""Send one directive, or a sequence of them, to the instrument.
|
|
65
|
+
|
|
66
|
+
A directive sets the instrument's frequency, its timeline and the like.
|
|
67
|
+
The unit keeps them, and puts them on the device at the next trigger of
|
|
68
|
+
the instrument. Applying after a run means the run's result can no
|
|
69
|
+
longer be fetched. Returns True once the unit has taken them.
|
|
70
|
+
"""
|
|
55
71
|
if isinstance(directive, Sequence):
|
|
56
72
|
return await self._agent.configure(self._token, self._id, directive)
|
|
57
73
|
else:
|
|
58
74
|
return await self._agent.configure(self._token, self._id, [directive])
|
|
59
75
|
|
|
60
76
|
async def initialize(self):
|
|
61
|
-
|
|
77
|
+
"""Initialize the instrument. See `Session.initialize`."""
|
|
78
|
+
await self._session.initialize([self._id])
|
|
62
79
|
|
|
63
80
|
@property
|
|
64
81
|
def instrument_config(self) -> C:
|
|
82
|
+
"""The hardware timing of the instrument, such as its sampling period."""
|
|
65
83
|
return self._config
|
|
66
84
|
|
|
67
85
|
async def fetch_result(self) -> ResultContainer:
|
|
68
|
-
|
|
69
|
-
return
|
|
86
|
+
"""Fetch the result of the instrument. See `Session.fetch_result`."""
|
|
87
|
+
return await self._session.fetch_result(self._id)
|
|
70
88
|
|
|
71
89
|
async def wait_for_result(
|
|
72
90
|
self, timeout_sec: float | None = None
|
|
@@ -74,8 +92,9 @@ class InstrumentDriver(Generic[D, C, P]):
|
|
|
74
92
|
"""Block until the result is ready, retrying transient fetch timeouts.
|
|
75
93
|
|
|
76
94
|
``timeout_sec`` caps the total client-side wait; ``None`` is unlimited.
|
|
95
|
+
See `Session.wait_for_result`.
|
|
77
96
|
"""
|
|
78
|
-
return await self.
|
|
97
|
+
return await self._session.wait_for_result(self._id, timeout_sec)
|
|
79
98
|
|
|
80
99
|
|
|
81
100
|
FixedTimelineInstrumentDriver: TypeAlias = InstrumentDriver[
|
|
@@ -88,6 +107,17 @@ FixedTimelineInstrumentDriver: TypeAlias = InstrumentDriver[
|
|
|
88
107
|
def create_instrument_driver_fixed_timeline(
|
|
89
108
|
session: Session, instrument_info: InstrumentInfo
|
|
90
109
|
) -> FixedTimelineInstrumentDriver:
|
|
110
|
+
"""Create a driver for a fixed-timeline instrument of the session.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
session: An open session that holds the instrument.
|
|
114
|
+
instrument_info: The instrument, as `Session.deploy_instruments()` or
|
|
115
|
+
the instrument resolver returns it.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ValueError: If the instrument is not in fixed-timeline mode, or not
|
|
119
|
+
held by the session.
|
|
120
|
+
"""
|
|
91
121
|
if instrument_info.definition.mode is not InstrumentMode.FIXED_TIMELINE:
|
|
92
122
|
raise ValueError(
|
|
93
123
|
f"Instrument mode is mismatched: '{instrument_info.definition.mode}' "
|
|
@@ -106,6 +136,7 @@ def create_instrument_driver_fixed_timeline(
|
|
|
106
136
|
instrument_info.definition,
|
|
107
137
|
instrument_info.config,
|
|
108
138
|
session.agent_container.instrument(extract_unit_label(instrument_info.id)),
|
|
139
|
+
session,
|
|
109
140
|
)
|
|
110
141
|
|
|
111
142
|
|
|
@@ -78,11 +78,12 @@ class InstrumentAgentGrpc(InstrumentAgent):
|
|
|
78
78
|
return True
|
|
79
79
|
|
|
80
80
|
@override
|
|
81
|
-
async def
|
|
81
|
+
async def arm(
|
|
82
82
|
self,
|
|
83
83
|
token: SessionToken,
|
|
84
84
|
resource_ids: Collection[ResourceId],
|
|
85
85
|
) -> bool:
|
|
86
|
+
# the Apply RPC arms the instruments; it will be renamed Arm
|
|
86
87
|
req = pb_inst.ApplyRequest(resource_ids=list(resource_ids))
|
|
87
88
|
metadata = dict(self._service.metadata or {})
|
|
88
89
|
metadata["x-session-token"] = str(token)
|