quelware-client 0.7.2__tar.gz → 0.7.4__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.4}/CHANGELOG.md +22 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/PKG-INFO +1 -1
- quelware_client-0.7.4/examples/fixed_timeline.py +173 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/pyproject.toml +1 -1
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/_session.py +229 -19
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/exceptions/__init__.py +51 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/instrument_driver/__init__.py +35 -4
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/interfaces/instrument_agent/__init__.py +7 -1
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/infra/instrument_agent_grpc/__init__.py +16 -1
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/testing/instrument_agent_mock/__init__.py +7 -1
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/core/instrument_driver/test_instrument_driver.py +8 -0
- quelware_client-0.7.4/tests/core/session/test_abort_runs.py +104 -0
- quelware_client-0.7.4/tests/core/session/test_not_triggered.py +286 -0
- quelware_client-0.7.4/tests/core/session/test_unit_busy.py +241 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/core/session/test_wait_for_results.py +18 -6
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/infra/instrument_agent_grpc/test_retry_on_connection_lost.py +2 -1
- {quelware_client-0.7.2 → quelware_client-0.7.4}/uv.lock +1 -1
- {quelware_client-0.7.2 → quelware_client-0.7.4}/.gitignore +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/LICENSE +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/Makefile +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/README.md +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/client/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/client/_grpc.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/client/_standalone_grpc.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/client/helpers/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/client/helpers/instrument_resolver/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/client/helpers/sequencer/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/_agent_container.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/_client.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/_utils.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/interfaces/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/interfaces/diagnostics_agent/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/interfaces/health_agent/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/interfaces/pat_provider/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/interfaces/resource_agent/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/interfaces/session_agent/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/interfaces/system_configuration_agent/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/interfaces/trigger_agent/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/interfaces/worker_agent/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/trigger_count_proposer/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/unit_control/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/infra/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/infra/_grpc_retry.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/infra/_timeouts.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/infra/diagnostics_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/infra/health_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/infra/pat_provider_file/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/infra/resource_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/infra/session_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/infra/system_configuration_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/infra/trigger_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/infra/worker_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/py.typed +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/testing/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/testing/mock_client.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/testing/resource_agent_mock/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/testing/session_agent_mock/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/testing/system_configuration_agent_mock/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/testing/trigger_agent_mock/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/testing/worker_agent_mock/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/core/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/core/client/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/core/client/helpers/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/core/client/helpers/test_instrument_resolver.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/core/client/helpers/test_sequencer.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/core/client/test_client.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/core/instrument_driver/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/core/session/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/core/session/test_session.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/core/session/test_trigger.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/infra/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/infra/health_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/infra/health_agent_grpc/test_health_check.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/infra/instrument_agent_grpc/__init__.py +0 -0
- {quelware_client-0.7.2 → quelware_client-0.7.4}/tests/infra/instrument_agent_grpc/test_wait_for_result.py +0 -0
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.7.4] - 2026-11-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Session.abort_runs()`, which stops the session's runs on every unit, before the trigger time or part way through. A run stops as a whole, because its instruments share the unit's waveform generators and captures, so it takes no instruments. Waiting for an instrument of a run raises the server's abort error. `AbortFailedError` is raised, once every unit has been asked, when some units failed; `failures` holds the error of each, by unit.
|
|
10
|
+
- `UnitBusyError` is raised also by `Session.abort_runs` while a trigger or initialize of the session is running, and by a trigger or initialize while `abort_runs` is.
|
|
11
|
+
|
|
12
|
+
## [0.7.3] - 2026-10-01
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `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.
|
|
17
|
+
- `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.
|
|
18
|
+
- `UnitBusyError`: `Session.trigger` and `Session.initialize` raise it immediately if a trigger or initialize of the same session is already running on the unit.
|
|
19
|
+
- `InitializeFailedError`: `Session.initialize` raises it, once every unit has finished, when some units failed; `failures` holds the error of each, by unit.
|
|
20
|
+
- `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.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `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.
|
|
25
|
+
- `InstrumentAgent.apply` is renamed `arm`, because it arms the instruments (`InstrumentDriver.apply` configures an instrument). It still calls the `Apply` RPC.
|
|
26
|
+
|
|
5
27
|
## [0.7.2] - 2026-09-29
|
|
6
28
|
|
|
7
29
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: quelware-client
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.4
|
|
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,14 @@ 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
|
+
AbortFailedError,
|
|
21
|
+
InitializeFailedError,
|
|
22
|
+
NotTriggeredError,
|
|
23
|
+
RunFailedError,
|
|
24
|
+
ServiceUnavailableError,
|
|
25
|
+
UnitBusyError,
|
|
26
|
+
)
|
|
19
27
|
from quelware_client.core.trigger_count_proposer import (
|
|
20
28
|
FixedOffsetTriggerCountProposer,
|
|
21
29
|
TriggerCountProposer,
|
|
@@ -45,6 +53,13 @@ class Session:
|
|
|
45
53
|
await session.trigger(instrument_ids)
|
|
46
54
|
results = await session.wait_for_results(instrument_ids)
|
|
47
55
|
```
|
|
56
|
+
|
|
57
|
+
Within a session, each unit has at most one run. A new trigger on a unit
|
|
58
|
+
replaces its run, and initializing an instrument on the unit clears it.
|
|
59
|
+
Other sessions on the same unit have their own runs. The session remembers
|
|
60
|
+
which instruments were in the last trigger on each unit, and waits only for
|
|
61
|
+
those. Another `Session` object built with the same token does not share
|
|
62
|
+
this record.
|
|
48
63
|
"""
|
|
49
64
|
|
|
50
65
|
def __init__( # noqa: PLR0913
|
|
@@ -90,6 +105,10 @@ class Session:
|
|
|
90
105
|
self._unit_to_ids.setdefault(ul, []).append(rid)
|
|
91
106
|
|
|
92
107
|
self._check_lock = not skip_lock_check
|
|
108
|
+
# instruments in the last trigger on each unit of this session
|
|
109
|
+
self._triggered: set[ResourceId] = set()
|
|
110
|
+
# units where a trigger, initialize or abort_runs of this session is running now
|
|
111
|
+
self._busy_units: set[UnitLabel] = set()
|
|
93
112
|
|
|
94
113
|
async def open(self):
|
|
95
114
|
"""Open the session, locking its resources and obtaining a token.
|
|
@@ -262,33 +281,53 @@ class Session:
|
|
|
262
281
|
instrument_ids: Collection[ResourceId],
|
|
263
282
|
wait_ms: int | None = None,
|
|
264
283
|
) -> int:
|
|
265
|
-
"""
|
|
284
|
+
"""Arm the given instruments, then trigger them.
|
|
266
285
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
286
|
+
Arming puts the configuration of each instrument on its unit's device,
|
|
287
|
+
as the run of the session there. Then the server schedules a trigger,
|
|
288
|
+
at which every unit starts. If the server's trigger service is
|
|
289
|
+
unavailable, the client triggers the units itself: a self-timed trigger
|
|
290
|
+
for a single unit, or a clock-synchronized trigger across several.
|
|
271
291
|
|
|
272
292
|
Args:
|
|
273
293
|
instrument_ids: Instruments to trigger.
|
|
274
|
-
wait_ms: Delay, in milliseconds, from now to when the
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
294
|
+
wait_ms: Delay, in milliseconds, from now to when the run starts,
|
|
295
|
+
giving every unit time to start on it. ``None`` leaves it to
|
|
296
|
+
the server, which also raises a wait shorter than it allows.
|
|
297
|
+
When the client triggers the units itself, it waits at least
|
|
298
|
+
500 ms.
|
|
279
299
|
|
|
280
300
|
Returns:
|
|
281
301
|
The clock count at which the trigger was scheduled.
|
|
302
|
+
|
|
303
|
+
Raises:
|
|
304
|
+
UnitBusyError: Immediately, if a trigger, initialize or abort_runs of this
|
|
305
|
+
session is already running on one of the units.
|
|
282
306
|
"""
|
|
283
307
|
unit_to_ids = create_unit_to_ids_map(instrument_ids)
|
|
308
|
+
with self._claiming(unit_to_ids):
|
|
309
|
+
# arming replaces the session's run on the unit, so forget the old
|
|
310
|
+
# one now, even if this trigger fails
|
|
311
|
+
self._forget_runs_on(unit_to_ids)
|
|
312
|
+
scheduled = await self._arm_and_trigger(
|
|
313
|
+
instrument_ids, unit_to_ids, wait_ms
|
|
314
|
+
)
|
|
315
|
+
self._triggered.update(instrument_ids)
|
|
316
|
+
return scheduled
|
|
284
317
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
318
|
+
async def _arm_and_trigger(
|
|
319
|
+
self,
|
|
320
|
+
instrument_ids: Collection[ResourceId],
|
|
321
|
+
unit_to_ids: dict[UnitLabel, list[ResourceId]],
|
|
322
|
+
wait_ms: int | None,
|
|
323
|
+
) -> int:
|
|
324
|
+
logger.info(f"starting arming (token= {self.token} )")
|
|
325
|
+
arm_coros = [
|
|
326
|
+
self._agent.instrument(unit_label).arm(self.token, ids)
|
|
288
327
|
for unit_label, ids in unit_to_ids.items()
|
|
289
328
|
]
|
|
290
|
-
await asyncio.gather(*
|
|
291
|
-
logger.info(f"finished
|
|
329
|
+
await asyncio.gather(*arm_coros)
|
|
330
|
+
logger.info(f"finished arming (token= {self.token} )")
|
|
292
331
|
|
|
293
332
|
try:
|
|
294
333
|
scheduled = await self._agent.trigger.trigger(
|
|
@@ -314,8 +353,10 @@ class Session:
|
|
|
314
353
|
) -> dict[ResourceId, ResultContainer]:
|
|
315
354
|
"""Wait for the result of every one of the instruments of a run.
|
|
316
355
|
|
|
317
|
-
Pass the instruments given to `trigger()
|
|
318
|
-
capture window is waited for
|
|
356
|
+
Pass the instruments given to `trigger()`. An instrument without a
|
|
357
|
+
capture window is waited for too, and its result is empty. Only the
|
|
358
|
+
instruments in the last trigger on each unit can be waited for, and not
|
|
359
|
+
after an instrument on that unit is initialized.
|
|
319
360
|
|
|
320
361
|
Args:
|
|
321
362
|
instrument_ids: Instruments to wait for.
|
|
@@ -325,11 +366,14 @@ class Session:
|
|
|
325
366
|
Each instrument's result.
|
|
326
367
|
|
|
327
368
|
Raises:
|
|
369
|
+
NotTriggeredError: Immediately, if an instrument was not in the last
|
|
370
|
+
trigger on its unit.
|
|
328
371
|
RunFailedError: If any instrument failed, once all of them have
|
|
329
372
|
finished, with the error of each that failed and the results of
|
|
330
373
|
the others.
|
|
331
374
|
TimeoutError: If some are still running when ``timeout_sec`` runs out.
|
|
332
375
|
"""
|
|
376
|
+
self._ensure_triggered(instrument_ids)
|
|
333
377
|
tasks = {
|
|
334
378
|
asyncio.ensure_future(
|
|
335
379
|
self._agent.instrument(extract_unit_label(rid)).wait_for_result(
|
|
@@ -361,6 +405,172 @@ class Session:
|
|
|
361
405
|
raise RunFailedError(failures, results)
|
|
362
406
|
return results
|
|
363
407
|
|
|
408
|
+
async def initialize(
|
|
409
|
+
self, instrument_ids: Collection[ResourceId], parallel: bool = True
|
|
410
|
+
) -> None:
|
|
411
|
+
"""Initialize the given instruments.
|
|
412
|
+
|
|
413
|
+
An initialized instrument is empty: armed, it plays and captures
|
|
414
|
+
nothing until it is given a timeline again. This also clears the
|
|
415
|
+
session's run on each of their units: no instrument on those units can
|
|
416
|
+
be waited for until the next trigger there.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
instrument_ids: Instruments to initialize.
|
|
420
|
+
parallel: Initialize the units at the same time, the default; when
|
|
421
|
+
False, one unit after another. Either way every unit is
|
|
422
|
+
initialized, also after one fails, and the instruments of one
|
|
423
|
+
unit are always initialized in one call.
|
|
424
|
+
|
|
425
|
+
Raises:
|
|
426
|
+
UnitBusyError: Immediately, if a trigger, initialize or abort_runs of this
|
|
427
|
+
session is already running on one of the units.
|
|
428
|
+
InitializeFailedError: If some units failed, once every unit has
|
|
429
|
+
finished, with the error of each unit that failed.
|
|
430
|
+
"""
|
|
431
|
+
unit_to_ids = create_unit_to_ids_map(instrument_ids)
|
|
432
|
+
with self._claiming(unit_to_ids):
|
|
433
|
+
self._forget_runs_on(unit_to_ids)
|
|
434
|
+
if parallel:
|
|
435
|
+
# wait for every unit, so that none is still initializing once
|
|
436
|
+
# the units are let go
|
|
437
|
+
outcomes = await asyncio.gather(
|
|
438
|
+
*(
|
|
439
|
+
self._initialize_unit(unit_label, ids)
|
|
440
|
+
for unit_label, ids in unit_to_ids.items()
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
else:
|
|
444
|
+
outcomes = [
|
|
445
|
+
await self._initialize_unit(unit_label, ids)
|
|
446
|
+
for unit_label, ids in unit_to_ids.items()
|
|
447
|
+
]
|
|
448
|
+
failures = {
|
|
449
|
+
unit_label: outcome
|
|
450
|
+
for unit_label, outcome in zip(unit_to_ids, outcomes, strict=True)
|
|
451
|
+
if outcome is not None
|
|
452
|
+
}
|
|
453
|
+
if failures:
|
|
454
|
+
raise InitializeFailedError(failures, len(unit_to_ids))
|
|
455
|
+
|
|
456
|
+
async def _initialize_unit(
|
|
457
|
+
self, unit_label: UnitLabel, instrument_ids: list[ResourceId]
|
|
458
|
+
) -> Exception | None:
|
|
459
|
+
"""Initialize the instruments of a unit, and return its error, if any."""
|
|
460
|
+
try:
|
|
461
|
+
await self._agent.instrument(unit_label).initialize(
|
|
462
|
+
self.token, instrument_ids
|
|
463
|
+
)
|
|
464
|
+
except Exception as e:
|
|
465
|
+
return e
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
async def abort_runs(self) -> None:
|
|
469
|
+
"""Stop the session's runs, on every unit.
|
|
470
|
+
|
|
471
|
+
The run on a unit is the session's last trigger there, and it stops as
|
|
472
|
+
a whole: its instruments share the unit's waveform generators and
|
|
473
|
+
captures, so one of them cannot stop without the others. Every
|
|
474
|
+
instrument of the runs is aborted, and waiting for one raises the
|
|
475
|
+
server's abort error. A unit stops its run before the trigger time
|
|
476
|
+
comes, or part way through, and leaves a run that has already finished
|
|
477
|
+
as it is. The instruments keep their configuration, so the same runs
|
|
478
|
+
can be triggered again.
|
|
479
|
+
|
|
480
|
+
Raises:
|
|
481
|
+
UnitBusyError: Immediately, if a trigger, initialize or abort_runs of this
|
|
482
|
+
session is running on any unit, since the run it makes is not
|
|
483
|
+
known yet.
|
|
484
|
+
AbortFailedError: If some units failed, once every unit has been
|
|
485
|
+
asked, with the error of each unit that failed.
|
|
486
|
+
"""
|
|
487
|
+
runs = {
|
|
488
|
+
unit_label: sorted(ids)
|
|
489
|
+
for unit_label, ids in create_unit_to_ids_map(self._triggered).items()
|
|
490
|
+
}
|
|
491
|
+
with self._claiming(set(runs) | self._busy_units):
|
|
492
|
+
outcomes = await asyncio.gather(
|
|
493
|
+
*(
|
|
494
|
+
self._agent.instrument(unit_label).abort(self.token, ids)
|
|
495
|
+
for unit_label, ids in runs.items()
|
|
496
|
+
),
|
|
497
|
+
return_exceptions=True,
|
|
498
|
+
)
|
|
499
|
+
failures = {
|
|
500
|
+
unit_label: outcome
|
|
501
|
+
for unit_label, outcome in zip(runs, outcomes, strict=True)
|
|
502
|
+
if isinstance(outcome, BaseException)
|
|
503
|
+
}
|
|
504
|
+
if failures:
|
|
505
|
+
raise AbortFailedError(failures, len(runs))
|
|
506
|
+
|
|
507
|
+
async def fetch_result(self, instrument_id: ResourceId) -> ResultContainer:
|
|
508
|
+
"""Fetch the result of one instrument, with a single request.
|
|
509
|
+
|
|
510
|
+
Unlike `wait_for_result()`, this does not retry when the request times
|
|
511
|
+
out.
|
|
512
|
+
|
|
513
|
+
Raises:
|
|
514
|
+
NotTriggeredError: Immediately, if the instrument was not in the last
|
|
515
|
+
trigger on its unit.
|
|
516
|
+
"""
|
|
517
|
+
self._ensure_triggered([instrument_id])
|
|
518
|
+
agent = self._agent.instrument(extract_unit_label(instrument_id))
|
|
519
|
+
return await agent.fetch_result(self.token, instrument_id)
|
|
520
|
+
|
|
521
|
+
async def wait_for_result(
|
|
522
|
+
self, instrument_id: ResourceId, timeout_sec: float | None = None
|
|
523
|
+
) -> ResultContainer:
|
|
524
|
+
"""Wait for the result of one instrument.
|
|
525
|
+
|
|
526
|
+
Unlike `wait_for_results()`, this raises the instrument's own error if
|
|
527
|
+
it failed.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
instrument_id: Instrument to wait for.
|
|
531
|
+
timeout_sec: Maximum time to wait; ``None`` waits without limit.
|
|
532
|
+
|
|
533
|
+
Raises:
|
|
534
|
+
NotTriggeredError: Immediately, if the instrument was not in the last
|
|
535
|
+
trigger on its unit.
|
|
536
|
+
"""
|
|
537
|
+
self._ensure_triggered([instrument_id])
|
|
538
|
+
agent = self._agent.instrument(extract_unit_label(instrument_id))
|
|
539
|
+
return await agent.wait_for_result(self.token, instrument_id, timeout_sec)
|
|
540
|
+
|
|
541
|
+
@contextmanager
|
|
542
|
+
def _claiming(self, unit_labels: Collection[UnitLabel]) -> Iterator[None]:
|
|
543
|
+
"""Mark the units as busy during a trigger, initialize or abort_runs.
|
|
544
|
+
|
|
545
|
+
Two of them at the same time on one unit would make the record of
|
|
546
|
+
triggered instruments wrong.
|
|
547
|
+
"""
|
|
548
|
+
units = set(unit_labels)
|
|
549
|
+
if busy := sorted(units & self._busy_units):
|
|
550
|
+
raise UnitBusyError().with_unit_labels(busy)
|
|
551
|
+
self._busy_units |= units
|
|
552
|
+
try:
|
|
553
|
+
yield
|
|
554
|
+
finally:
|
|
555
|
+
self._busy_units -= units
|
|
556
|
+
|
|
557
|
+
def _forget_runs_on(self, unit_labels: Collection[UnitLabel]) -> None:
|
|
558
|
+
"""Forget the triggered instruments on the units."""
|
|
559
|
+
units = set(unit_labels)
|
|
560
|
+
self._triggered = {
|
|
561
|
+
rid for rid in self._triggered if extract_unit_label(rid) not in units
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
def _ensure_triggered(self, instrument_ids: Collection[ResourceId]) -> None:
|
|
565
|
+
"""Raise if an instrument was not in the last trigger on its unit.
|
|
566
|
+
|
|
567
|
+
Raises:
|
|
568
|
+
NotTriggeredError: With the ids of those instruments.
|
|
569
|
+
"""
|
|
570
|
+
missing = sorted(set(instrument_ids) - self._triggered)
|
|
571
|
+
if missing:
|
|
572
|
+
raise NotTriggeredError().with_resource_ids(missing)
|
|
573
|
+
|
|
364
574
|
async def _client_side_trigger_fallback(
|
|
365
575
|
self,
|
|
366
576
|
unit_to_ids: dict[UnitLabel, list[ResourceId]],
|
{quelware_client-0.7.2 → quelware_client-0.7.4}/src/quelware_client/core/exceptions/__init__.py
RENAMED
|
@@ -101,3 +101,54 @@ 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 AbortFailedError(QuelwareClientError):
|
|
122
|
+
"""Raised when abort_runs failed on some units.
|
|
123
|
+
|
|
124
|
+
``failures`` holds the error of each unit that failed, by its label. The
|
|
125
|
+
other units were aborted.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(self, failures: Mapping[UnitLabel, BaseException], units: int):
|
|
129
|
+
self.failures = dict(failures)
|
|
130
|
+
detail = "; ".join(f"{u}: {e}" for u, e in sorted(self.failures.items()))
|
|
131
|
+
super().__init__(
|
|
132
|
+
f"abort_runs failed on {len(self.failures)} of {units} units: {detail}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class NotTriggeredError(_ErrorWithResourceIdsMixin, QuelwareClientError):
|
|
137
|
+
"""Raised when waiting for an instrument that was not triggered.
|
|
138
|
+
|
|
139
|
+
Within a session, each unit has at most one run. A new trigger on the unit
|
|
140
|
+
replaces it, and initializing an instrument on the unit clears it. So only
|
|
141
|
+
the instruments in the last trigger on each unit can be waited for.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
DEFAULT_MESSAGE = "The instruments were not in the last trigger on their unit."
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class UnitBusyError(_ErrorWithUnitLabelsMixin, QuelwareClientError):
|
|
148
|
+
"""Raised when a trigger, initialize or abort_runs of the same session is already
|
|
149
|
+
running on the unit."""
|
|
150
|
+
|
|
151
|
+
DEFAULT_MESSAGE = (
|
|
152
|
+
"A trigger, initialize or abort_runs of this session is already running "
|
|
153
|
+
"on the unit."
|
|
154
|
+
)
|