Timing Rules¶
LabOne Q follows strict rules to determine the timing of pulses and sections in the sample-precise, real-time parts of an experiment. This page defines those rules and clarifies how timing grids influence alignment, scheduling, and duration quantization.
For relevant background, see Sections and Pulses.
Key Concepts¶
Timing Grid¶
A timing grid defines the discrete time points at which events may begin or end in a compiled experiment. When an element (such as a pulse, section, or acquisition) has a timing grid, it means that its start time, end time, and therefore its duration must align to integer multiples of that grid spacing.
Fundamentally, timing grids arise from the digital nature of the instrument (e.g. digital clock rates). By respecting timing grids, LabOne Q can ensure that events are repeatable with sample-by-sample precision, and the timing constraints between instruments remain tractable.
Two timing grids are especially important in LabOne Q and deserve explicit names: the signal timing grid and the system timing grid.
Signal Timing Grid¶
The signal timing grid, defines the finest time resolution available on a given signal line. All pulses and delays on that signal must start and end on this grid. It coincides with the sample period of the digital hardware, so we also sometimes call it the sample grid.
In LabOne Q, pulse or delay length are typically specified in seconds. LabOne Q rounds the start and the duration to the nearest sample on the relevant instrument. If the pulse length is not an integer multiple of the sampling period, this rounding will slightly stretch or shrink the pulse envelope. Rounding of the pulse boundaries conserves waveform memory, and guarantees that multiple instances of the same pulse are identical, sample-by-sample. The carrier (and hence the phase of the pulse) are unaffected by this adjustment.
System Timing Grid¶
The system timing grid is a coarser grid used for coordinating events across multiple instruments or hardware subsystems. It is derived from the least common multiple (LCM) of the signal timing grids and any additional clock domains involved in an experiment. In other words, this timing grid is commensurate with all of the clock domains in the experiment.
Features that require system-wide coordination are referred to as system-level features and are always aligned to the system timing grid. Examples include acquisitions, parameter sweeps, and trigger outputs.
Timing Reference Table¶
| Instruments | Signal Timing Grid [ns] | System Timing Grid [ns] |
|---|---|---|
| HDAWG | 1/2.4 ≃ 0.417 | 16/2.4 ≃ 6.67 |
| UHFQA | 1/1.8 ≃ 0.556 | 8/1.8 ≃ 4.45 |
| SHF* | 1/2 = 0.5 | 16/2 = 8 |
| HDAWG + UHFQA | N/A | 1000/GCD\(\left(\frac{2400}{16}, \frac{1800}{8}\right)\) ≃ 13.3 |
| HDAWG + SHF* | 1/2 = 0.5 | 16/2 = 8 |
Rules¶
The following is a set of rules that determine the timing of pulses played as part of any Experiment in LabOne Q.
-
Pulses are aligned to the signal timing grid of the signal line they are bound to.
-
Pulses on the same signal line are scheduled sequentially. Pulses on separate signal lines are scheduled in parallel, unless constrained by section-level settings or dependencies.
-
System-level features such as acquisitions, parameter sweeps and trigger outputs are aligned to the system timing grid.
-
A section’s timing grid is commensurate with all contained elements. When more than one grid is present, it is set to their least common multiple (LCM). In almost all practical cases, this coarser grid is equal to the system grid.
-
When system-level features are present, the section is automatically escalated to the system grid.
-
If
on_system_grid = True, the section is explicitly escalated to the system grid.
-
-
The section’s start and end align to integer multiples of its own grid, while its internal elements remain on their respective grids.
-
If the section length does not fall on an integer multiple of the section’s timing grid, the compiler extends the section to the next valid grid point. Extension occurs regardless of whether the section’s length is determined by the timing of its contents, or set explicitly in the DSL.
-
Sections can be left-aligned or right-aligned. Alignment determines whether elements are scheduled from the start or from the end of the section’s timing window. For left-aligned sections, length extensions are added at the end. For right-aligned sections they are added at the start.
-
-
Sections that share a signal are scheduled sequentially. Sections that do not share any signals are scheduled in parallel, unless explicitly constrained by the user. For example, the reserve() command or the play_after argument can be used to control execution order. Note that
reserve()may escalate the section grid, andplay_afterdoes not.
Special Alignment Cases¶
Certain operations require grids coarser than the system grid. These include:
-
Branching statements (e.g., for active reset) where feedback data is routed globally are aligned to a 200 ns grid.
-
Oscillator phase resets for signals on the SHF series of instruments are aligned to a 40 ns grid.
The following operations do not escalate the grid:
-
Phase increments and oscillator switching remain on the signal grid. If the compiler cannot express the resulting timing pattern in SeqC, a compilation error may occur.
-
Precompensation clearing does not escalate the grid.
Tip
Tip: You can inspect the timing of sections and pulses after compilation using the Pulse Sheet. This tool helps visualize how LabOne Q applies the scheduling rules in practice, making it easier to debug misalignments, gaps, or unexpected extensions.
Illustrative Examples¶
The following examples illustrate how the compiler applies the timing rules introduced above.
Pulses and Delays on a Single Signal¶
Consider the following simplified DSL snippet:
...
play("signal1", pulse, length=8)
delay("signal1", 9)
play("signal1", pulse, length=6)
...
Assume the instrument driving signal1 has a sample period of 1 s. (Units are arbitrary here, chosen for clarity.)
Because all commands act on a single signal, timing is determined solely by the signal timing grid (Rule 1). This means the compiler schedules each pulse such that it starts and ends on integer multiples of the sample period. The pulses and delays are scheduled sequentially (Rule 2). Figure 1 shows the corresponding timing diagram.
Adding a Section¶
Now wrap the same commands inside a section:
...
with section():
play("signal1", pulse, length=9)
delay("signal1", 10)
play("signal1", pulse, length=6)
...
The resulting timing is unchanged, as shown in
Figure 2. When a section contains only elements on a single signal, the section inherits that signal’s timing grid. This follows from Rule 4.
Sequential Sections¶
Next, add a second section on the same signal:
...
with section():
play("signal1", pulse, length=9)
delay("signal1", duration=10)
play("signal1", pulse, length=6)
with section():
play("signal1", pulse, length=7)
...
In
Figure 3, Sections 1 and 2 share the same signal. Per Rule 6, these sections are scheduled sequentially. Both sections inherit their timing grid from
signal1, so the compiler places the two sections back-to-back with no gap.
In the context of scheduling, it can be helpful to abstract away the internal contents of each section, and think of each section as an opaque box characterized by a grid and a duration. The scheduler arranges these boxes on the timeline while respecting their alignment rules, as illustrated in Figure 4.
Conceptually, sections and pulses are treated identically by the scheduler: each is an opaque time block aligned to its respective grid. The compiler arranges them left-to-right (or right-to-left, if the section’s alignment mode specifies) as closely as possible without overlap, extending them only as needed to satisfy grid alignment.
Sections Involving Multiple Signals¶
Scheduling gets more complex when a section involves multiple signals with different sampling rates. Suppose signal1 and signal2 belong to instruments with sample periods of 1 s and 1.5 s respectively:
...
with section():
delay("signal1", 11)
delay("signal2", 4.5)
play("signal1", pulse, length=12) # Pulse 1
play("signal2", pulse, length=4.5) # Pulse 2
...
Each pulse still aligns perfectly to its own signal grid. However, the section itself must start and end on a grid that is commensurate with both signals (Rule 4). The compiler therefore sets the section grid to LCM(1 s, 1.5 s) = 3 s.
In Figure 5, note the small gap between the end of the last pulse and the end of the section. This is the extension required to satisfy section-level alignment. Sections are left-aligned by default, so the extension is placed at the end of the section (Rule 5).
Separate Sections with Different Grids¶
Consider two signals with different sampling periods: signal1 has a period of 1 s, and signal2 has a period of 3 s.
We first define a section containing three pulses on signal1 (the blue section), followed by a section containing one pulse on signal2 (the orange section):
...
with section(uid = "blue_section"):
play("signal1", pulse, length=4)
delay("signal1", 1)
play("signal1", pulse, length=4)
delay("signal1", 1)
play("signal1", pulse, length=4)
with section(uid = "orange_section", play_after = "blue_section"):
play("signal2", pulse, length=9)
...
Here, the orange section is set to play after the blue section. As shown in Figure 6, the orange section begins at the first valid point on its 3 s timing grid after the blue section has finished. Because the two sections operate on different timing grids and no common alignment is enforced, a gap appears between them.
To remove this gap and make the sections play seamlessly back-to-back, we can wrap both sections inside a parent section whose timing grid is commensurate with both signals. By setting the parent section to right-aligned, its end (and thus the end of the orange section) will align to the coarser grid while keeping both subsections synchronized.
...
with section(alignment = SectionAlignment.RIGHT):
with section(uid = "blue_section"):
play("signal1", pulse, length=4)
delay("signal1", 1)
play("signal1", pulse, length=4)
delay("signal1", 1)
play("signal1", pulse, length=4)
with section(uid = "orange_section", play_after = "blue_section"):
play("signal2", pulse, length=9)
...
As shown in Figure 7, the blue and orange sections now align perfectly: the blue section ends exactly where the orange section begins. Because the 1 s grid of signal1 divides evenly into the 3 s grid of signal2, a right-aligned parent section allows both to end on the same grid point. The compiler can therefore schedule them flush without any gap.