The logical programming stack

This guide uses a few small examples to show how CUDA-Q Logical turns an idea into something you can place on fault-tolerant hardware and estimate. In this short walk-through, you will write and place a Bell-pair program, define a quantum-error-correction (QEC) realization, and inspect its outputs.

CUDA-Q Logical formalizes the layers of abstraction involved in this process as P0, P1, P2, and P3:

P0: logical program → P1: placement → P2: QEC realization → P3: physical schedule
       │                    │                  │                    │
 logical estimate     region and slots   static/analytical    timed events
                                             │
                                         Stim text

Each compilation stage adds detail without changing the behavior of the original program.

1. Write the logical program

Start with a program that prepares and measures a Bell pair:

Define, compile, and estimate a Bell program (examples/standalone/00_logical_program.py).
import cudaq.logical as cql


# %%
# Define a Bell program directly with linear logical-qubit values.
@cql.program
def bell() -> tuple[bool, bool]:
    qubits = cql.allocate(2, state=cql.types.zero)
    qubits[0] = cql.h(qubits[0])
    qubits[0], qubits[1] = cql.cx(qubits[0], qubits[1])
    return cql.measure_z(qubits[0]), cql.measure_z(qubits[1])


# %%
# Compile the program and request its logical resource counts directly.
build = cql.compile(bell)
resources = cql.estimate(build, tier=cql.estimate.Tier.LOGICAL)

The program says what to compute, but not where to place the qubits or which QEC code to use. That makes it portable.

The assignments are important. A quantum value has one live owner, so an operation consumes the current value and returns its successor. For example, qubits[0] = cudaq.logical.h(qubits[0]) replaces the old value of qubits[0] with the one returned by h. This rule prevents stale or duplicated quantum values from reaching the compiled program.

cudaq.logical.compile(bell) produces an immutable P0 build. At this point, a logical estimate can count the program’s logical qubits and operations:

Portable Bell program: 2 logical qubits

It cannot yet count encoded patches or syndrome rounds because you have not chosen a realization. Those figures become available later, when the compiler has the facts needed to calculate them.

2. Place the qubits on a logical machine

Next, describe the available logical machine. This one has a compute region with two slots and supports logical computation and measurement:

Define a two-slot logical machine (examples/standalone/01_logical_placement.py).
@cql.machine
class TwoSlotMachine:
    compute = cql.architecture.region(
        capabilities=(
            cql.architecture.capability.logical_compute,
            cql.architecture.capability.logical_measurement,
        ),
        capacity=2,
    )


Keep this information out of the Bell program. The same program can then be placed on another compatible machine, and the same machine can host other programs.

Compile the program and ask the placement solver to keep its two data qubits together:

Place the Bell program and inspect the result.
@cql.program
def bell() -> tuple[bool, bool]:
    qubits = cql.allocate(2, state=cql.types.zero, name="data")
    qubits[0] = cql.h(qubits[0])
    qubits[0], qubits[1] = cql.cx(qubits[0], qubits[1])
    return cql.measure_z(qubits[0]), cql.measure_z(qubits[1])


# %%
# Compile and place the logical owners into the machine's compute region.
logical = cql.compile(bell)
placed = cql.compiler.place(
    logical,
    device=TwoSlotMachine,
    placement=(cql.architecture.colocate(logical.values.data),),

The resulting P1 build records the region and slot assigned to each logical value:

Bell data[0:2] placed on compute[0:2]

Placement refines the P0 build; it does not retrace or rewrite the Python program. The recorded placement is also replayable, as the final assertion in the example demonstrates.

3. Define a QEC realization

At P2, codes and gadgets describe how to realize logical behavior. The next example defines the Steane code, an objective for terminal memory, and a gadget that implements that objective:

Define a Steane code and terminal-memory gadget (examples/standalone/02_code_and_gadget.py).
import cudaq.logical as cql


# %%
# Define the self-dual Steane CSS code from its check supports.
@cql.code
class Steane:
    block = cql.codes.CSSBlock(data=7, sx=3, sz=3)
    d = 3
    hx = ((0, 1, 2, 3), (0, 1, 4, 5), (0, 2, 4, 6))
    hz = hx
    lx = (tuple(range(7)),)
    lz = lx


# %%
# Declare the ideal logical operation implemented by the gadget.
@cql.objective
def terminal_memory(qubit: cql.types.logical_qubit) -> None:
    cql.discard(qubit)


# %%
# Implement that operation using encoded syndrome extraction and measurement.
@cql.gadget(implements=terminal_memory)
def steane_memory(block: cql.patch[Steane]) -> None:
    block, _ = cql.extract_syndrome(block)
    block, _ = cql.mz(block.data)
    cql.discard(block)

The code supplies its CSS checks, logical operators, and distance. The gadget works on a typed Steane patch, extracts its syndrome, measures its data qubits, and ends their lifetime.

implements=terminal_memory is a checked claim about the gadget’s behavior. CUDA-Q Logical compares the gadget with the objective using their types and derived actions; a matching Python name alone is not enough.

Materialize the code and compile the gadget to inspect the verified P2 definitions and their operation counts:

code = cql.materialize(Steane)
gadget = cql.compile(steane_memory)
counts = cql.analysis.count(gadget)

assert "fabric.code @Steane" in code.to_mlir()
assert "fabric.gadget @steane_memory" in gadget.to_mlir()
assert (Steane.n, Steane.k, Steane.d.value) == (7, 1, 3)
Steane [[7,1,3]] terminal-memory gadget:
  authored operations: {'reset': 2, 'h': 2, 'cx': 2, 'read_syndrome_ancillas': 1, 'mz': 1, 'dealloc': 1}

The assertions are useful beyond testing the example: they show which code and gadget reached the P2 representation and which code properties were available to selection.

4. Estimate a selected realization

CUDA-Q Logical offers four estimation tiers:

Tier

Available from

What it reports

LOGICAL

P0

logical qubits, actions, and instruments

STATIC

P2

encoded patches, gadget calls, and authored actions

ANALYTICAL

P2

modeled physical cost, error, acceptance, and timing

SCHEDULE

P3

physical resources, timed events, and utilization

You can also enter this pipeline from a regular CUDA-Q kernel. The following example selects a distance-3 surface-code target and asks cudaq.estimate for the cost of preparing and measuring a logical zero:

Estimate a CUDA-Q kernel with a surface-code target (examples/02_surface_code_resource_estimate.py).
import cudaq.logical as cql


# %%
# Author a logical-zero memory kernel whose qubit demand is a parameter.
@cudaq.kernel
def logical_zero_memory(logical_qubits: int):
    qubits = cudaq.qvector(logical_qubits)
    mz(qubits)


# %%
# Establish a one-qubit, distance-three reference configuration.
baseline_qubits = 1
baseline_target = cql.targets.surface_physical_target(
    logical_capacity=baseline_qubits,
    distance=3,
)
cudaq.set_target(baseline_target)
baseline_target.print_stack()

baseline_estimate = cudaq.estimate(logical_zero_memory, baseline_qubits)
baseline_analytical = baseline_estimate.annotations["ANALYTICAL"]
baseline_schedule = baseline_estimate.annotations["SCHEDULE"]

The baseline estimate carries logical, static, analytical, and schedule annotations. The rest of the example varies the code distance, logical capacity, error rate, failure budget, and cycle time to show which layout and timing metrics change. These are resource estimates, not results from a noise simulation or hardware execution.

5. Emit a verified gadget as Stim

A selected P2 program can also leave CUDA-Q Logical as standards-compatible Stim circuit text. It is an interchange format, not another compilation stage: the output contains the explicit resets, Clifford operations, and measurements of the selected gadget, and CUDA-Q Logical does not use it to sample or decode detector events. If the required realization is missing, emission stops at that boundary instead of filling in an implementation.

The projection runs through qlx-translate; see Stim emission for the command and its fail-closed boundaries.

What each stage owns

Each kind of information belongs to a specific stage:

Stage

Adds

Does not change

P0 logical build

logical actions and value ownership

P1 placed build

regions, slot bindings, placement evidence

requested logical behavior

P2 QEC realization

codes, patches, gadgets, and protocol details

logical behavior or placement

P3 physical build

carriers, routing, native events, and schedule

requested logical behavior

Some verified facts sit alongside a stage rather than extending this sequence. CUDA-Q Logical calls them facets. Code specifications, gadget realizations, protocol networks, patch graphs, carrier mappings, routing, and physical schedules are facets that downstream tools can request and inspect.

Builds retain this evidence and can be serialized and replayed. When a required fact is absent, CUDA-Q Logical reports the missing requirement rather than choosing a machine, code, or gadget on your behalf.

When reading or writing CUDA-Q Logical code, two questions are usually enough to orient yourself:

  1. Which stage or facet owns this fact?

  2. Did I supply it, or can CUDA-Q Logical derive and verify it?

Where to go next