Actions

Blocks and pipes describe the geometry layout of the surface code computation. Actions name logical measurements, combine their results, and use those values to choose a measurement basis, apply a Pauli correction, or keep a shot. Bloq derives the physical readout recipes and their dependencies during compilation.

Action kinds

Action

BLOG example

Effect

Named measurement

mzz = measure 1 -> +X

Name a logical observable on a block or spatial pipe

Binding

odd = m1 ^ m2

Name a Boolean expression

Resolve

resolve 5 if odd or resolve correction if odd

Select a measurement basis or a branch geometry

Pauli feedback

feedback Z 1 -> +Z if odd

Apply a logical correction at a block or wire target

Discard

discard if bad

Reject the shot when the condition is true

Targets refer to the authored spacetime layout. A block reference can be its ID or coordinate, such as [0, 0, 1]. A pipe reference adds a direction from one endpoint, such as [0, 0, 1] -> +X. These coordinates identify logical geometry, while the compiler determines physical qubits and execution times.

Named measurement

name = measure target names a logical observable already measured by the geometry. Its value is a bit: zero for eigenvalue \(+1\) and one for eigenvalue \(-1\).

For a block target, the patch terminates at a measured top face, facing forward in time. The observable has a correlation surface whose support at that face agrees with its measurement basis. For example, an XZX cube has an X top face, so mx = measure block_id names its logical X measurement. Fixed X and Z caps and a terminal Y block supply their corresponding bases. A selective cap supplies the basis chosen by its resolve action. A block with a continuing temporal wire does not become a terminal measurement merely because a measure statement names it. Ports and T resources are not block measurement targets.

For a pipe target, the pipe must be spatial, along X or Y. It represents lattice surgery between patches. The named observable is the joint parity whose correlation surface crosses that pipe in the surgery measurement basis. For an ordinary pipe, this is the Pauli basis complementary to its time face. For example, the spatial pipe between the two XZX cubes in the T example below measures \(Z\otimes Z\), even though its direction is +X. The direction locates the pipe and does not specify the Pauli basis. Hadamard pipes exchange the endpoint frames, so their support must be interpreted in each local frame. Temporal pipes continue a patch and cannot be named parity measurement targets.

The named value combines physical records, signs, frame terms, and the decoder’s flip estimate. Conditions use this corrected logical value. Bloq must find an output safe readout in every reachable choice. Naming a relation that touches a live output does not authorize measuring that output. See Correlation Surfaces for the readout closure rule.

Binding

odd = m1 ^ m2 gives a name to a Boolean expression. It has no block or pipe target and adds no quantum operation. Its inputs can be named measurements, other bindings, or declared classical module inputs.

Operator, from highest to lowest precedence

Meaning

!

NOT

&

AND

^

XOR

|

OR

Parentheses make grouping explicit, as in odd = (m1 ^ m2) & !bad. All operands must be available. An OR expression still waits for both inputs when one is already true.

Resolve

resolve target if condition selects one of two authored alternatives.

For a selective measurement block, the target is the cap’s position or ID. The condition chooses its Pauli basis. In XY, true selects X and false selects Y. In XZ, true selects X and false selects Z. In YZ, true selects Y and false selects Z. Reversed spellings reverse this assignment and are canonicalized when saved. The choice acts at the cap’s time boundary and must wait for its corrected selector before the measurement can execute.

For a structural branch, the target is its name, such as correction. True selects the true geometry and false selects the false geometry. The arms occupy a bounded spacetime region with matching external connections. Only the selected arm executes. It reconnects to the shared continuation if the region has one. Both arms remain in the source. Supported arms cannot contain nested branches, Ports, T resources, or selective caps.

Pauli feedback

feedback Z target if odd applies a logical Pauli correction when the condition is true. Its target locates where the logical operator acts in the spacetime layout.

For a block target, such as feedback Z block_id if odd, Bloq selects an outgoing temporal wire at the block, then an incoming temporal wire, then a sole spatial wire. A multiplex Port selects its retained logical output. The block reference therefore places the correction on a particular logical continuation.

For a wire target, such as feedback Z block_id -> +Z if odd, the direction explicitly selects the incident pipe from that block’s endpoint. Use this form to distinguish wires when several meet at a block. The Pauli is expressed in the selected endpoint’s frame. Across a Hadamard pipe, X and Z exchange, so moving the correction to the other end requires changing its basis.

Interpret either target through the correlation surfaces that cross it. At the selected wire, compare the feedback Pauli with each surface’s local Pauli support. Anticommuting support flips the sign of that relation and hence the logical observable it carries. For example, Z feedback flips an X observable whose surface crosses the target with X support. The observable’s bit is XORed with odd. Z or identity support commutes with the same feedback and contributes no flip. A geometric intersection alone does not determine the effect, since the Pauli labels and endpoint frames matter. Bloq uses these crossings to propagate feedback into named readouts and output frame relations.

Feedback can list several targets separated by commas. Omitting if makes it unconditional. For each correlation surface, the contributions combine by XOR, including repeated targets. An odd number of anticommuting crossings flips its observable, while two identical corrections cancel.

Discard

discard if bad rejects the current shot when its condition is true. It has no geometric target and waits for the values in its condition. The runtime cancels work that has not yet issued once rejection is known. It does not undo operations that have already executed.

Discard is postselection on the computation. A T resource’s cultivation retry protocol instead repeats that resource preparation inside its compiled region. Discard does not request a retry of the whole computation.

Action DAG

Actions form a directed acyclic graph, or DAG. Each node is an action and each arrow points from a prerequisite to its consumer. Named values give explicit dependencies, such as a measurement feeding a resolve. Analysis also finds dependencies from correlation surfaces: a readout can wait for a selected cap, a branch containing its support, an earlier corrected parity, or feedback that changes its sign.

from pathlib import Path

import bloq

source = bloq.GalleryItem.THREE_BIT_ADDER.load()
dag = source.analyze_action_graph()

Path("three-bit-adder-action-dag.svg").write_text(dag.to_svg(), encoding="utf-8")
    use bloq::prelude::*;

    let source = GalleryItem::ThreeBitAdder.build();
    let dag = source.analyze_action_graph()?;

    std::fs::write("three-bit-adder-action-dag.svg", dag.to_svg()?)?;

The complete three bit adder graph below contains 51 actions.

Complete analyzed source action DAG for the three bit adder with measurements, bindings, branch resolves, feedback and their dependencies.

Source line order does not set start times. Independent actions can proceed concurrently, while a cycle means the required values cannot be produced in a causal order.

Examples

Logical T gate

The data patch runs from [0, 0, 0] to [0, 0, 2]. A cultivated T resource enters beside it at [1, 0, 0]. Two XZX cubes join through a spatial pipe, and an XY cap terminates the resource patch. Build the layout and its two actions directly:

import bloq


def build_graph():
    graph = bloq.BlockGraph()
    for position, kind in [
        ((0, 0, 0), "Port"),
        ((0, 0, 1), "XZX"),
        ((0, 0, 2), "Port"),
        ((1, 0, 0), "T"),
        ((1, 0, 1), "XZX"),
        ((1, 0, 2), "XY"),
    ]:
        graph.add_block(bloq.Block(position, kind))
    for position, direction in [
        ((0, 0, 0), "+Z"),
        ((0, 0, 1), "+Z"),
        ((0, 0, 1), "+X"),
        ((1, 0, 0), "+Z"),
        ((1, 0, 1), "+Z"),
    ]:
        graph.add_pipe(bloq.Pipe(position, direction))
    graph.set_actions([
        bloq.Action.measure(bloq.MeasureTarget.edge((0, 0, 1), "+X"), "mzz"),
        bloq.Action.resolve((1, 0, 2), ~bloq.Expr.var("mzz")),
    ])
    graph.validate()
    return graph
use bloq::graph::{Action, Expr, MeasureTarget, SelectiveKind};
use bloq::prelude::*;

fn build_graph() -> Result<BlockGraph> {
    let mut graph = BlockGraph::new();
    for (position, kind) in [
        ([0, 0, 0], BlockKind::Port),
        ([0, 0, 1], BlockKind::Cube(CubeKind::XZX)),
        ([0, 0, 2], BlockKind::Port),
        ([1, 0, 0], BlockKind::T),
        ([1, 0, 1], BlockKind::Cube(CubeKind::XZX)),
        ([1, 0, 2], BlockKind::Selective(SelectiveKind::XY)),
    ] {
        graph.try_add_block(Block::new(position, kind))?;
    }
    for (position, direction) in [
        ([0, 0, 0], Direction::ZPLUS),
        ([0, 0, 1], Direction::ZPLUS),
        ([0, 0, 1], Direction::XPLUS),
        ([1, 0, 0], Direction::ZPLUS),
        ([1, 0, 1], Direction::ZPLUS),
    ] {
        graph.try_add_pipe(Pipe::new(position, direction))?;
    }
    graph.set_actions(vec![
        Action::Measure {
            target: MeasureTarget::Edge {
                src: IVec3::new(0, 0, 1),
                dir: Direction::XPLUS,
            },
            name: "mzz".into(),
        },
        Action::Resolve {
            target: IVec3::new(1, 0, 2),
            condition: Expr::Not(Box::new(Expr::Var("mzz".into()))),
        },
    ])?;
    graph.validate()?;
    Ok(graph)
}
BLOG 1.0

module main {
  in q0: data = 0
  out q1: data = 2

  0: Port [0, 0, 0]
  1: XZX [0, 0, 1]
  2: Port [0, 0, 2]
  3: T [1, 0, 0]
  4: XZX [1, 0, 1]
  5: XY [1, 0, 2]
  0 -> +Z
  1 -> +Z
  1 -> +X
  3 -> +Z
  4 -> +Z

  mzz = measure 1 -> +X
  resolve 5 if !mzz

}

The same graph with the mzz correlation surface:

mzz names the \(Z\otimes Z\) parity measured by the spatial pipe at [0, 0, 1] -> +X. The Bloq compiler constructs a corresponding correlation surface for this measurement action and uses it to derive the physical readout recipe for mzz. Its corrected value controls the resource cap. resolve [1, 0, 2] if !mzz selects X when mzz is zero and Y when it is one. The compiler derives the remaining Pauli frame for the surviving data output. The logical T gate tutorial continues with physical execution and output checks.

CCZ gate teleportation

This layout consumes an externally supplied three qubit \(|CCZ\rangle\) state through the temporal input Ports at [0, 2, 0], [1, 2, 0], and [2, 2, 0]. Three multiplex Ports at [0, 0, 1], [1, 0, 1], and [2, 0, 1] couple the data to the resource while retaining the data outputs. Three correction regions choose either X measurement caps or a geometry implementing a CZ correction. The source supplies the teleportation protocol, without preparing the CCZ state.

import bloq


def build_graph():
    graph = bloq.BlockGraph()
    for position, kind, role, tag in [
        ((0, 2, 0), "Port", "input", "CCZ_0"),
        ((1, 2, 0), "Port", "input", "CCZ_1"),
        ((2, 2, 0), "Port", "input", "CCZ_2"),
        ((0, 0, 1), "Port", "multiplex", "x"),
        ((0, 1, 1), "ZXX", "auto", ""),
        ((0, 2, 1), "ZXX", "auto", ""),
        ((1, 0, 1), "Port", "multiplex", "y"),
        ((1, 1, 1), "ZZX", "auto", ""),
        ((1, 2, 1), "ZXX", "auto", ""),
        ((2, 0, 1), "Port", "multiplex", "z"),
        ((2, 1, 1), "ZZX", "auto", ""),
        ((2, 2, 1), "ZXX", "auto", ""),
        ((1, 3, 1), "ZZX", "auto", ""),
        ((0, 3, 1), "XZX", "auto", ""),
        ((2, 3, 1), "ZXX", "auto", ""),
    ]:
        graph.add_block(bloq.Block(position, kind, role=role, tag=tag))
    for position, direction in [
        ((0, 2, 0), "+Z"),
        ((1, 2, 0), "+Z"),
        ((0, 0, 1), "+Y"),
        ((0, 1, 1), "+Y"),
        ((1, 0, 1), "+Y"),
        ((1, 1, 1), "+Y"),
        ((1, 2, 1), "+Y"),
        ((2, 0, 1), "+Y"),
        ((2, 1, 1), "+Y"),
        ((2, 2, 1), "-Z"),
        ((2, 2, 1), "+Y"),
        ((1, 3, 1), "-X"),
    ]:
        graph.add_pipe(bloq.Pipe(position, direction))

    graph.add_branches(
        [
            bloq.Branch(
                "b0", "m0x",
                on_false=bloq.BranchArm([((2, 3, 2), "X"), ((1, 2, 2), "X")]),
                on_true=bloq.BranchArm(
                    [((2, 3, 2), "ZXZ"), ((1, 2, 2), "ZXX"), ((1, 3, 2), "ZZX")],
                    pipes=[bloq.Pipe((2, 3, 2), "-X", hadamard=True),
                           bloq.Pipe((1, 3, 2), "-Y")],
                ),
            ),
            bloq.Branch(
                "b1", "m1y",
                on_false=bloq.BranchArm([((0, 1, 2), "X"), ((2, 2, 2), "X")]),
                on_true=bloq.BranchArm(
                    [((0, 1, 2), "ZXZ"), ((2, 2, 2), "ZXX"),
                     ((2, 1, 2), "ZZX"), ((1, 1, 2), "ZZX")],
                    pipes=[bloq.Pipe((2, 2, 2), "-Y"),
                           bloq.Pipe((2, 1, 2), "-X"),
                           bloq.Pipe((1, 1, 2), "-X", hadamard=True)],
                ),
            ),
            bloq.Branch(
                "b2", "m2z",
                on_false=bloq.BranchArm([((0, 3, 2), "X"), ((0, 2, 2), "X")]),
                on_true=bloq.BranchArm(
                    [((0, 3, 2), "XZZ"), ((0, 2, 2), "ZXX")],
                    pipes=[bloq.Pipe((0, 2, 2), "+Y", hadamard=True)],
                ),
            ),
        ],
        pipes=[bloq.Pipe(position, "+Z") for position in
               [(2, 3, 1), (1, 2, 1), (0, 1, 1), (2, 2, 1), (0, 3, 1), (0, 2, 1)]],
        actions=[bloq.Action.measure(bloq.MeasureTarget.edge((x, 0, 1), "+Y"), name)
                 for x, name in enumerate(["m0x", "m1y", "m2z"])],
    )
    m0x, m1y, m2z = (bloq.Expr.var(name) for name in ["m0x", "m1y", "m2z"])
    for position, condition in [
        ((2, 0, 1), m0x & m1y),
        ((1, 0, 1), m0x & m2z),
        ((0, 0, 1), m1y & m2z),
    ]:
        graph.add_action(bloq.Action.feedback([("Z", position)], condition=condition))
    graph.validate()
    return graph
use bloq::graph::{Expr, parse_actions};
use bloq::prelude::*;

fn build_graph() -> Result<BlockGraph> {
    let mut graph = BlockGraph::new();
    for (position, kind, role, tag) in [
        ([0, 2, 0], "Port", PortRole::Input, "CCZ_0"),
        ([1, 2, 0], "Port", PortRole::Input, "CCZ_1"),
        ([2, 2, 0], "Port", PortRole::Input, "CCZ_2"),
        ([0, 0, 1], "Port", PortRole::Multiplex, "x"),
        ([0, 1, 1], "ZXX", PortRole::Auto, ""),
        ([0, 2, 1], "ZXX", PortRole::Auto, ""),
        ([1, 0, 1], "Port", PortRole::Multiplex, "y"),
        ([1, 1, 1], "ZZX", PortRole::Auto, ""),
        ([1, 2, 1], "ZXX", PortRole::Auto, ""),
        ([2, 0, 1], "Port", PortRole::Multiplex, "z"),
        ([2, 1, 1], "ZZX", PortRole::Auto, ""),
        ([2, 2, 1], "ZXX", PortRole::Auto, ""),
        ([1, 3, 1], "ZZX", PortRole::Auto, ""),
        ([0, 3, 1], "XZX", PortRole::Auto, ""),
        ([2, 3, 1], "ZXX", PortRole::Auto, ""),
    ] {
        let mut block = Block::new(position, kind.parse::<BlockKind>()?).with_tag(tag)?;
        if block.kind().is_port() {
            block = block.with_port_role(role)?;
        }
        graph.try_add_block(block)?;
    }
    for (position, direction) in [
        ([0, 2, 0], Direction::ZPLUS),
        ([1, 2, 0], Direction::ZPLUS),
        ([0, 0, 1], Direction::YPLUS),
        ([0, 1, 1], Direction::YPLUS),
        ([1, 0, 1], Direction::YPLUS),
        ([1, 1, 1], Direction::YPLUS),
        ([1, 2, 1], Direction::YPLUS),
        ([2, 0, 1], Direction::YPLUS),
        ([2, 1, 1], Direction::YPLUS),
        ([2, 2, 1], Direction::ZMINUS),
        ([2, 2, 1], Direction::YPLUS),
        ([1, 3, 1], Direction::XMINUS),
    ] {
        graph.try_add_pipe(Pipe::new(position, direction))?;
    }
    graph.try_add_branches(
        [
            Branch::new(
                "b0",
                Expr::Var("m0x".into()),
                BranchArm::try_from_blocks([([2, 3, 2], "X"), ([1, 2, 2], "X")])?,
                BranchArm::try_from_blocks([
                    ([2, 3, 2], "ZXZ"),
                    ([1, 2, 2], "ZXX"),
                    ([1, 3, 2], "ZZX"),
                ])?
                .with_pipes([
                    Pipe::new([2, 3, 2], Direction::XMINUS).with_hadamard(),
                    Pipe::new([1, 3, 2], Direction::YMINUS),
                ]),
            ),
            Branch::new(
                "b1",
                Expr::Var("m1y".into()),
                BranchArm::try_from_blocks([([0, 1, 2], "X"), ([2, 2, 2], "X")])?,
                BranchArm::try_from_blocks([
                    ([0, 1, 2], "ZXZ"),
                    ([2, 2, 2], "ZXX"),
                    ([2, 1, 2], "ZZX"),
                    ([1, 1, 2], "ZZX"),
                ])?
                .with_pipes([
                    Pipe::new([2, 2, 2], Direction::YMINUS),
                    Pipe::new([2, 1, 2], Direction::XMINUS),
                    Pipe::new([1, 1, 2], Direction::XMINUS).with_hadamard(),
                ]),
            ),
            Branch::new(
                "b2",
                Expr::Var("m2z".into()),
                BranchArm::try_from_blocks([([0, 3, 2], "X"), ([0, 2, 2], "X")])?,
                BranchArm::try_from_blocks([([0, 3, 2], "XZZ"), ([0, 2, 2], "ZXX")])?
                    .with_pipes([Pipe::new([0, 2, 2], Direction::YPLUS).with_hadamard()]),
            ),
        ],
        [
            [2, 3, 1],
            [1, 2, 1],
            [0, 1, 1],
            [2, 2, 1],
            [0, 3, 1],
            [0, 2, 1],
        ]
        .map(|position| Pipe::new(position, Direction::ZPLUS)),
        parse_actions(
            r"m0x = measure [0, 0, 1] -> +Y
m1y = measure [1, 0, 1] -> +Y
m2z = measure [2, 0, 1] -> +Y
feedback Z [2, 0, 1] if m0x & m1y
feedback Z [1, 0, 1] if m0x & m2z
feedback Z [0, 0, 1] if m1y & m2z",
            |_, _| None,
        )?,
    )?;
    graph.validate()?;
    Ok(graph)
}
BLOG 1.0

module main {
  in q0: data = 0
  in q1: data = 1
  in q2: data = 2
  in q3: data = 3
  in q4: data = 6
  in q5: data = 9

  0: Port [0, 2, 0] role=input <CCZ_0>
  1: Port [1, 2, 0] role=input <CCZ_1>
  2: Port [2, 2, 0] role=input <CCZ_2>
  3: Port [0, 0, 1] role=multiplex <x>
  4: ZXX [0, 1, 1]
  5: ZXX [0, 2, 1]
  6: Port [1, 0, 1] role=multiplex <y>
  7: ZZX [1, 1, 1]
  8: ZXX [1, 2, 1]
  9: Port [2, 0, 1] role=multiplex <z>
  10: ZZX [2, 1, 1]
  11: ZXX [2, 2, 1]
  12: ZZX [1, 3, 1]
  13: XZX [0, 3, 1]
  14: ZXX [2, 3, 1]
  3 -> +Y
  4 -> +Z
  4 -> +Y
  0 -> +Z
  5 -> +Z
  13 -> +Z
  6 -> +Y
  7 -> +Y
  1 -> +Z
  8 -> +Z
  8 -> +Y
  12 -> -X
  9 -> +Y
  10 -> +Y
  11 -> -Z
  11 -> +Z
  11 -> +Y
  14 -> +Z
  branch b0 {
    false {
      15: X [1, 2, 2]
      16: X [2, 3, 2]
    }
    true {
      17: ZXX [1, 2, 2]
      18: ZZX [1, 3, 2]
      19: ZXZ [2, 3, 2]
      18 -> -Y
      19 -H> -X
    }
  }
  branch b1 {
    false {
      20: X [0, 1, 2]
      21: X [2, 2, 2]
    }
    true {
      22: ZXZ [0, 1, 2]
      23: ZZX [1, 1, 2]
      24: ZZX [2, 1, 2]
      25: ZXX [2, 2, 2]
      23 -H> -X
      24 -> -X
      25 -> -Y
    }
  }
  branch b2 {
    false {
      26: X [0, 2, 2]
      27: X [0, 3, 2]
    }
    true {
      28: ZXX [0, 2, 2]
      29: XZZ [0, 3, 2]
      28 -H> +Y
    }
  }

  m0x = measure 3 -> +Y
  m1y = measure 6 -> +Y
  m2z = measure 9 -> +Y
  resolve b0 if m0x
  resolve b1 if m1y
  resolve b2 if m2z
  feedback Z 9 if m0x & m1y
  feedback Z 6 if m0x & m2z
  feedback Z 3 if m1y & m2z

}

The three measurement correlation surfaces are shown with all correction branches projected to their true arms:

m0x, m1y, and m2z name the three joint measurements at the multiplex connections. The suffixes label the data wires. Each outcome selects one CZ correction region: b0, b1, or b2. False selects the pair of X caps, while true selects the connected correction geometry. The remaining Pauli corrections depend on pairs of outcomes:

Retained data output

Z feedback condition

z at [2, 0, 1]

m0x & m1y

y at [1, 0, 1]

m0x & m2z

x at [0, 0, 1]

m1y & m2z

These nonlinear conditions and the three branch selectors remain separate actions in the DAG. They consume corrected values from the same signed correlation relation.

Continue with Modules to compose geometry and classical interfaces, then BLOG Format to write the complete source as text.