Triggering¶
This page explains how a run of instruments starts, how you get its results, and how it can fail. The tutorial shows the same steps as code.
A run, step by step¶
A run takes three calls:
- Configure each instrument with
driver.apply(): its frequency, its timeline, and so on. The unit keeps these settings; nothing plays yet. - Trigger the instruments with
session.trigger(). The client first arms them, so the unit puts their settings on the device, and then asks for a trigger at a time a little after now. Every unit starts at that time, so instruments on several units start together. - Wait for the results with
session.wait_for_results(). It returns when every instrument has finished, with the result of each.
sequenceDiagram
participant C as Client
participant M as Central server
participant U as Unit
C->>U: Configure (driver.apply)
C->>U: Arm (in session.trigger)
C->>M: Trigger (in session.trigger)
M->>U: Start at count N
U-->>C: (the run plays from count N)
C->>U: Fetch the result (session.wait_for_results)
U-->>C: Result, or the error of the run
await driver.apply([SetFrequency(hz=6.0e9), timeline])
await session.trigger(ids)
results = await session.wait_for_results(ids, timeout_sec=10.0)
Pass wait_for_results() the same instruments you gave to trigger(), also
the ones that only send. An instrument without a capture window has an empty
result, but waiting for it tells you whether it played: if it failed, you get
its error instead of a capture that silently lacks a pulse.
One run per unit, in a session¶
Within a session, each unit has at most one run:
- A trigger on a unit replaces the run before it. After you trigger instrument B, you can no longer wait for instrument A on the same unit, unless you trigger A again.
- Initializing an instrument (
session.initialize()ordriver.initialize()) clears the run of its unit, for every instrument on that unit. The instrument is left empty: armed, it plays and captures nothing until you give it a timeline again. - Units are separate: a trigger on unit X keeps the run on unit Y.
- Sessions are separate: other sessions on the same unit have their own runs, and never replace yours.
The client keeps track of this. When you wait for an instrument that is not in
the last trigger on its unit, it raises NotTriggeredError at once, without
asking the unit. This also covers an instrument whose trigger failed.
A trigger and an initialize change the run of a unit, so one session must not
run two of them on the same unit at the same time. If you try, for example by
calling trigger() twice in parallel, the second one raises UnitBusyError at
once. Calls on different units can run in parallel.
Note
The client keeps this record per Session object. If you build a second
Session with the same token, it does not see the triggers of the first.
When the trigger fires¶
trigger() takes wait_ms, the time from now to when the run starts. It gives
every unit time to get ready.
- With
wait_ms=None, the default, the server chooses the wait, by how it is set up and by how many units the run spans. - The server raises a wait shorter than it allows to its minimum.
A unit that is not ready when the time comes cannot start the run, and reports
an error (see below). Use the default unless you have measured that a shorter
wait works on your system;
quel-trigger-wait-test measures
it.
When a run fails¶
A unit reports an error. For example, the unit could not start the run at
the time it was given. wait_for_results() waits for every instrument to
finish, then raises RunFailedError:
failuresholds the error of each instrument that failed, as the unit reported it.resultsholds the results of the others.
try:
results = await session.wait_for_results(ids, timeout_sec=10.0)
except RunFailedError as e:
for instrument_id, error in e.failures.items():
print(f"{instrument_id} failed: {error}")
results = e.results
The trigger itself fails. If a unit refuses to start the run, trigger()
raises the unit's error. Nothing has run, so waiting for these instruments
raises NotTriggeredError. You can trigger them again.
The run does not end. wait_for_results() waits without limit unless you
pass timeout_sec. When the time runs out, it raises TimeoutError with the
instruments that are still running. Always pass timeout_sec, a little longer
than your run takes.
You configure an instrument after its run. Calling driver.apply() after a
run means the unit no longer has the result of that run, and waiting for it
fails. Wait for the results first, then configure the next run.
Long runs and lost connections¶
You do not need to handle these yourself:
- A unit answers a fetch only for a limited time. When a run takes longer, the
client asks again, until the run ends or
timeout_secruns out. - Every call has a time limit, so a unit that is gone makes the call fail instead of hanging.
- When a connection is lost, the client retries the calls that are safe to make again. It does not retry the scheduling of a trigger, because a late one would start the run after its time.