The logical programming stack

Follow one Bell-pair computation from a portable logical program to a physical schedule using the Steane code. First, compile, place, and estimate the program directly with cudaq.logical. Then expose the same device as a CUDA-Q target so application developers can estimate a kernel with a single cudaq.estimate call.

Install CUDA-Q Logical following the quick start. See Core concepts for the definitions used throughout this guide.

The excerpts below come from the runnable Steane Bell-pair example. From the repository root, run:

python3 preview/logical/examples/steane_bell_e2e_workflow.py

The compilation stages are P0, P1, P2, and P3:

P0: logical program → P1: placement → P2: QEC realization → P3: physical schedule

Each stage adds the information needed for more detailed resource estimates.

1. Compile and estimate the logical program

Define a logical program that prepares and measures a Bell pair. The program specifies the computation independently of its QEC code and device:

import cudaq.logical as cql


@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])


The assignments retain the successor values returned by each operation; see linear ownership for why these assignments are required.

cql.compile translates the program into a P0 build. Pass that build to cql.estimate to count its logical resources:

logical = cql.compile(bell)
logical_resources = cql.estimate(logical, tier=cql.estimate.Tier.LOGICAL)

print(f"Peak logical qubits: {logical_resources.logical_qubits_peak}")
print(f"Hadamard actions: {logical_resources.actions['qlx_standard_h']}")
print(f"Controlled-X actions: {logical_resources.actions['qlx_standard_cx']}")
Peak logical qubits: 2
Hadamard actions: 1
Controlled-X actions: 1

These counts describe the workload. Encoded blocks, physical qubits, and timing depend on the realization we choose next.

2. Choose the code and its implementations

Define the Steane [[7,1,3]] CSS code explicitly. It protects one logical qubit with seven data qubits; CSSBlock also reserves three X-check and three Z-check ancillas. The hx and hz rows list the data-qubit indices for each X and Z stabilizer check, while lx and lz define the logical X and Z operators.

@cql.code validates the code’s algebraic structure. cql.materialize creates its compiler representation, which can be inspected independently of the Bell program:

@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


code = cql.materialize(Steane)

assert (Steane.n, Steane.k, Steane.d.value) == (7, 1, 3)
assert Steane.block.size == 13

The same code is also available as the built-in cql.codes.Steane. To use it, replace the class definition with Steane = cql.codes.Steane.

Supply gadgets for the Bell program’s Hadamard and controlled-X operations. Each implements= clause names the ideal logical action; the body describes its implementation on encoded patches. For Steane, these operations can be applied across the data qubits:

@cql.gadget(implements=cql.standard.h)
def steane_h(block: cql.patch[Steane]) -> cql.patch[Steane]:
    return cql.h(block.data)


@cql.gadget(implements=cql.standard.cx)
def steane_cx(
    control: cql.patch[Steane],
    target: cql.patch[Steane],
) -> tuple[cql.patch[Steane], cql.patch[Steane]]:
    return cql.cx(control.data, target.data)


Combine these gadgets with the supplied preparation and measurement gadgets in a QEC architecture. Together they cover every operation in the Bell program:

steane_architecture = cql.devices.QECArchitecture(
    "SteaneBellArchitecture",
    Steane.default_encoding,
    link_roots=(
        cql.gadgets.prepare_zero(Steane),
        steane_h,
        steane_cx,
        cql.gadgets.measure_z(Steane),
    ),
)

See Defining a code and Gadgets and verification for custom definitions.

3. Describe the device

A device connects the logical machine, QEC realization, and physical resources. First, give its logical machine a compute region with two slots, then bind that region to the Steane architecture:

builder = cql.devices.DeviceBuilder("SteaneBellDevice")
compute = builder.logical.add_compute(capacity=2)
encoded_region = builder.qec.bind(compute, architecture=steane_architecture)

Each logical qubit needs one Steane block. Here, CSSBlock(data=7, sx=3, sz=3) reserves seven data qubits and six check ancillas per block. The device therefore reserves 2 * (7 + 3 + 3) = 26 physical qubits: 14 data qubits and 12 ancillas. The ancillas are part of this example’s block layout. Declare the operations, measurements, and timing available on the physical machine:

carriers = builder.physical.add_qubits(
    2 * Steane.block.size,
    native_actions=cql.architecture.physical_actions.clifford_set(),
    native_instruments=(
        cql.architecture.physical_instruments.MZ,
        cql.architecture.physical_instruments.MPP,
    ),
)
builder.physical.bind(encoded_region, to=carriers)
builder.physical.set_operating_point(timing={"cycle_ns": 1.0})
device = builder.build()

This is a teaching device with an illustrative one-nanosecond cycle and native Clifford operations and Pauli-product measurements. Its resource and timing results describe these assumptions. See Devices and placement for other device configurations.

4. Place and realize the program

Use cql.compiler.place to assign the P0 program’s logical values to the device’s logical machine. The constraint keeps the named data allocation in one region:

placed = cql.compiler.place(
    logical,
    device=device,
    placement=(cql.architecture.colocate(logical.values.data),),
)

print([(binding.space, binding.slot) for binding in placed.placement.bindings])
[('compute', 0), ('compute', 1)]

This P1 build records placement. It has not yet selected the QEC implementations. Continue compilation with the QEC pipeline to produce P2, then estimate the encoded resources directly:

encoded = cql.compile(
    placed,
    pipeline=cql.compiler.pipelines.qec(),
    device=device,
)
static_resources = cql.estimate(encoded, tier=cql.estimate.Tier.STATIC)

print(f"Peak encoded patches: {static_resources.patches_peak}")
print(f"Hadamard gadget calls: {static_resources.gadget_calls['steane_h']}")
print(
    f"Controlled-X gadget calls: {static_resources.gadget_calls['steane_cx']}")
Peak encoded patches: 2
Hadamard gadget calls: 1
Controlled-X gadget calls: 1

The same Bell program now uses two Steane patches. Its selected implementations also include two preparations and two logical-Z measurements.

5. Schedule and estimate the physical resources

Continue with the physical pipeline and schedule the resulting P3 events:

physical = cql.compile(
    encoded,
    pipeline=cql.compiler.pipelines.physical(),
    device=device,
)
schedule = cql.compiler.schedule(physical)

cql.estimate can now report physical resources and timing. Keep the estimate options together so we can reuse them in the CUDA-Q target below:

estimate_options = {
    "p_phys": 1.0e-3,
    "failure_budget": 0.1,
    "cycle_time": 1.0e-9,
}
schedule_resources = cql.estimate(
    schedule,
    tier=cql.estimate.Tier.SCHEDULE,
    **estimate_options,
)

print(f"Physical qubits: {schedule_resources.physical_qubits}")
print(f"Scheduled events: {schedule_resources.event_count}")
print(f"Makespan: {schedule_resources.makespan_ns:.1f} ns")
Physical qubits: 26
Scheduled events: 13
Makespan: 4.0 ns

The estimation tier determines which resources to report and which compilation stage is required:

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

See Resource estimation for the analytical tier and the complete result fields. The outputs above are estimates for preparing and measuring the Bell pair under this device model.

6. Expose the device as a CUDA-Q target

The explicit calls above are useful when developing a device or inspecting compilation stages. For application developers, a compilation target packages that workflow so an ordinary CUDA-Q kernel can use the same code, gadgets, physical bindings, and estimate options.

Expose device through Target.from_device and select the resulting target:

target = cql.targets.Target.from_device(
    "steane_bell",
    device,
    estimate_options=estimate_options,
)
cudaq.set_target(target)

This target can now be used with CUDA-Q kernels. For example, the same logical program can be written as a Bell-pair kernel:

import cudaq


@cudaq.kernel
def bell_pair():
    qubits = cudaq.qvector(2)
    h(qubits[0])
    x.ctrl(qubits[0], qubits[1])
    mz(qubits)


The target now handles logical compilation, placement, QEC realization, and physical scheduling when the application calls cudaq.estimate. Recover the logical and static counts with typed views; the schedule results are available in the SCHEDULE annotation:

estimate = cudaq.estimate(bell_pair)
kernel_logical = cql.estimate.LogicalEstimate.from_annotations(
    estimate.annotations)
kernel_static = cql.estimate.FabricCounts.from_annotations(estimate.annotations)
kernel_schedule = estimate.annotations["SCHEDULE"]

assert kernel_logical.logical_qubits_peak == logical_resources.logical_qubits_peak
assert kernel_logical.actions == logical_resources.actions
assert kernel_static.patches_peak == static_resources.patches_peak
assert kernel_static.gadget_calls == static_resources.gadget_calls
assert kernel_schedule["physical_qubits"] == schedule_resources.physical_qubits
assert kernel_schedule["event_count"] == schedule_resources.event_count
assert kernel_schedule["makespan_ns"] == schedule_resources.makespan_ns

Both entry points estimate the same Bell-pair workload on the same device. An application can reuse the target for compatible kernels without repeating the individual compiler calls.

Where to go next