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
Execute an encoded program with
cudaq.runin the Steane Bernstein–Vazirani example. It supplies an encoding circuit and returns measurement bits per shot. The T-state distillation example runs on distance-1BareQubitpatches and adds non-Clifford gates, measurement feedback, and noise.Read Core concepts for definitions of programs, machines, codes, gadgets, devices, and targets.
Work through the guides for defining a code, placing a program, and estimating resources.
Browse the examples. The Carbon target builds a custom code and integrates paired operations with CUDA-Q kernels.