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:
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:
@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:
@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:
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:
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:
Which stage or facet owns this fact?
Did I supply it, or can CUDA-Q Logical derive and verify it?
Where to go next
Work through the task-oriented guides for defining a code, placing a program, and estimating resources.
Browse the complete set of runnable examples, including distillation, Clifford+T synthesis, and the Gidney–Ekerå projection.
Read Core concepts for linear ownership, evidence, and implementation discovery in more detail.
See the architecture reference for compiler stages, dialects, and pipelines.