Emulate Hardware Synchronization

This tutorial uses Bloq’s backend timing controls to emulate hardware waiting and synchronization in a T–H–T–H computation. Set input arrival times and decoder latency, then inspect how live patches stay protected while resources and classical results become ready.

The example runs in Bloq VM with a mock decoder. It models a controller’s waiting behavior. Device integration needs a custom backend with the hardware’s own timing and decoder.

Locate the waits in THTH

Load the THTH gallery graph. Its two T gates consume cultivated resources, and the corrected parities mzz1 and mzz2 select later measurement bases. The Hadamards connect the two injections.

Each injection needs both a data patch and an accepted T state. Its adaptive measurement also needs a decoded parity:

flowchart LR
    factory["T factory<br/>retry until accepted"] --> join["T injection<br/>wait for both patches"]
    input["Data patch arrives"] --> join
    join --> parity["Measure joint parity"]
    parity --> decoder["Wait for decoder result"]
    decoder --> cap["Select measurement basis"]

Waiting point

What must become ready

What happens meanwhile

T injection

Data patch and accepted T state

Independent preparation overlaps. The patch that arrives first stays protected.

Adaptive measurement

Corrected mzz1 or mzz2

The resource patch stays protected until its measurement basis is known.

Logical output

Final Pauli frame

The output patch stays protected until the frame is ready.

Use whole memory rounds for long waits and physical idle intervals for gaps shorter than one round. At a join, finish the current QEC round before starting the joint operation. See Synchronization for the IR’s memory templates and fixed-round padding APIs.

Dependency between the two T gates

The measurement selected by mzz2 must wait for the measurement selected by mzz1 to finish. The mzz2 correlation surface reaches the first selected measurement at (2, 1, 2), so its corrected parity needs that measurement’s records and frame corrections.

flowchart LR
    m1["Corrected mzz1"] --> cap1["First selected measurement<br/>finish and produce records"]
    cap1 --> m2["Assemble and decode mzz2"]
    joint2["Second joint measurement records"] --> m2
    m1 -->|Frame correction| m2
    m2 --> cap2["Second selected measurement"]

Compilation derives this dependency from the source correlation surfaces and represents it with node dependencies in Bloq IR. The first measurement’s quantum node feeds the classical readout nodes for corrected mzz2, which control the second measurement’s quantum node. Backends follow these dependencies automatically.

Independent T-state preparation and earlier surgery work can still overlap while the second selected measurement waits.

Configure the backend timing

Use bloq.lower_vm() in Python or LoweringConfig in Rust. Release times set when a source may start. Dependencies, decoded values, and occupied qubits can delay it further. Use one time unit for releases and moment durations.

Python control

Rust control

Value in the example

Purpose

gate_duration

LoweringConfig.gate_duration

1.0

Set the duration of each nonempty physical moment.

factory_release_time

SourceTiming.factory

0.0

Start T factories before the data arrives.

input_release_time

SourceTiming.input

23.0

Model a later arrival of the logical input.

source_release_time

SourceTiming.clifford

23.0

Release ordinary Clifford sources. Prepared-Y sources are shifted to finish with them.

decoder_latency_rounds

LoweringConfig.decoder_latency_rounds

10

Model decoder latency in local memory rounds.

Decoder waiting has two timing rules:

Query

Result-ready time

Ordinary logical readout

Ten memory-round durations after the last contributing measurement, or the request time, whichever is later. Independent work can overlap the latency.

Accepted T factory

After ten additional physical memory rounds following cultivation and escape. Earlier measurements do not shorten this hold. Rejected attempts skip it.

Factory retries make resource readiness vary between shots. A rejected attempt cancels unissued work, lets issued work finish, and replays the authored preparation. Only the accepted attempt exports records to later operations.

Run the example

Compile at distance 3 and run without noise. Ideal mock-decoder decisions keep this check focused on timing and the logical output.

import csv
import json
import math
from collections import Counter
from pathlib import Path

import bloq

# [example-start]
ir = bloq.compile(bloq.GalleryItem.THTH.load(), distance=3)
program = bloq.lower_vm(
    ir,
    gate_duration=1.0,
    decoder_latency_rounds=10,
    factory_release_time=0.0,
    source_release_time=23.0,
    input_release_time=23.0,
)
result = program.run(
    seed=17,
    input_state="plus",
    decoder_acceptance=1.0,
    accepted_accuracy=1.0,
    rejected_accuracy=1.0,
)
assert not result.discarded
expected = (math.sqrt(0.5), 0.5, 0.5)
assert all(math.isclose(a, b, abs_tol=1e-9) for a, b in zip(result.logical_bloch(), expected))
trace = result.trace
assert trace["metadata"]["decoder_latency_rounds"] == 10
assert all(span["end"] <= result.finished_at + 1e-9 for span in trace["timing"])
Path("thth.instructions.json").write_text(program.to_json(indent=2), encoding="utf-8")
Path("thth.trace.json").write_text(result.to_json(indent=2), encoding="utf-8")
print("logical Bloch vector:", result.logical_bloch())
print("finished at:", result.finished_at)
print("source roles:", dict(Counter(source["role"] for source in program.sources)))

Run the complete Python example with python thth.py in an empty output directory. It also exports thth.timeline.csv.

use bloq::{
    graph::GalleryItem,
    vm::{self, LoweringConfig, SourceTiming},
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // [example-start]
    let ir = bloq::compile::compile(&GalleryItem::THTH.build(), 3)?;
    let options = LoweringConfig {
        gate_duration: 1.0,
        decoder_latency_rounds: 10,
        source_timing: SourceTiming {
            factory: 0.0,
            input: 23.0,
            clifford: 23.0,
        },
        ..Default::default()
    };
    let program = vm::lower(&ir, &options)?;
    let mut runtime = options.runtime_config(17);
    runtime.decoder.acceptance_probability = 1.0;
    runtime.decoder.accepted_accuracy = 1.0;
    runtime.decoder.rejected_accuracy = 1.0;
    let result = program.run(runtime)?;
    assert!(!result.artifact.discarded);
    let actual = result.logical_bloch(&program.outputs[0])?;
    let expected = (0.5_f64.sqrt(), 0.5, 0.5);
    assert!((actual.0 - expected.0).abs() < 1e-9);
    assert!((actual.1 - expected.1).abs() < 1e-9);
    assert!((actual.2 - expected.2).abs() < 1e-9);
    std::fs::write("thth.instructions.json", program.to_json_pretty()?)?;
    std::fs::write("thth.trace.json", result.artifact.to_json_pretty()?)?;
    println!("logical Bloch vector: {actual:?}");
    println!("finished at: {}", result.artifact.finished_at);
    // [example-end]
    Ok(())
}

Run cargo run --locked --release --manifest-path docs/examples/Cargo.toml --bin thth in the source distribution.

Both examples check that the accepted, frame-corrected output of \(HTHT|+\rangle\) has Bloch vector

\[ (\langle X\rangle,\langle Y\rangle,\langle Z\rangle) =(1/\sqrt2,1/2,1/2). \]

Check discarded before reading the logical output. The mock decoder’s accuracy settings test control behavior. They do not provide a calibrated logical error rate.

Inspect waiting and synchronization in the trace

The examples export thth.instructions.json and thth.trace.json. Increase input_release_time to model later data arrival, or decoder_latency_rounds to model a slower decoder, then rerun the example. Compare source releases, actual starts, result-ready times, and protected waiting intervals in the trace:

Trace field or event

What to check

timing

Independent work overlaps. Joined operations wait for all participating work.

decoder_deadline

Compare measurement, request, and result-ready times.

memory_round

QEC continues during waits. wait_reason identifies synchronization, causal cuts, or decoder latency. Factory holds use memory_kind="factory_gap".

idle

Physical idle fills short gaps between protected operations.

The following saved noisy shot illustrates the same mechanisms. Time runs downward, with one column per logical site. Cultivation retries delay one factory, and QEC protects live patches during joins and decoder waits. Column width does not represent physical-qubit area.

THTH logical-site occupation over time, showing factory retries, QEC during synchronization and decoder waits, and short physical idle intervals.