Core concepts
Everything in CUDA-Q Logical follows from a small set of ideas. Learn these eight, and the rest of the system becomes predictable.
The package is cudaq.logical, which this documentation names in full when it
refers to a symbol. Code examples import it under the conventional short alias
and use that instead:
import cudaq.logical as cql
1. Four stages, one direction
Executable intent is refined through exactly four semantic stages
(cudaq.logical.stages.Stage):
Stage |
Question it answers |
Primary IR family |
|---|---|---|
P0 unplaced logical |
What logical computation is requested? |
|
P1 placed logical |
Where may each logical owner reside on a logical machine? |
|
P2 QEC realization |
Which code, gadgets, and protocols realize it, and at what cost? |
|
P3 physical schedule |
Which carriers and timed events physically realize it? |
|
A later stage only adds realization facts — it never silently reinterprets the
logical behavior you asked for. Every stage root is verified and immutable, so
when a lowering fails you fall back to the retained earlier root instead of
repairing in place. cudaq.logical.compile produces the P0 root of a
@cudaq.logical.program, cudaq.logical.compiler.place refines it to a
code-agnostic P1 placement, and selecting codes, gadgets, and protocols produces
P2. Physical lowering and scheduling produce P3. Emitting Stim text consumes a
P2 build; it is an interchange product, not a stage.
2. Facets, not extra stages
QEC specifications, gadget realizations, protocol networks, and patch graphs are
facets (cudaq.logical.stages.Facet). A facet is an independently verified
fact attached to an immutable stage root, not another point in the P0–P3
lowering order. Physical lowering and scheduling attach their own facets the
same way. Several facets can coexist on one root without recompiling the
program. Every pipeline pass declares the facets it requires, provides, and
invalidates; a facet survives a pass unless that pass explicitly invalidates or
recomputes it. A compiled gadget build, for example, is a P2 root carrying
exactly QEC_SPEC and QEC_REALIZATION — see build.facets in concept 5.
3. Linear ownership
Quantum values are linear: each value has one live owner, and every operation consumes that owner and produces its successor.
import cudaq.logical as cql
@cql.program
def ownership() -> tuple[bool, bool]:
q = cql.allocate(1, state=cql.types.zero)
q[0] = cql.h(q[0]) # consume q[0], produce its successor
q[0], z = cql.mpp(cql.types.Z(q[0])) # nondestructive: the owner survives
return z, cql.measure_z(q[0]) # destructive: q[0] is gone
Rebinding — q[0] = cudaq.logical.h(q[0]) — is how you write that contract.
Using a consumed value raises cudaq.logical.errors.UseAfterConsume at trace
time. The canonical IR checks the same contract independently: every linear SSA
value must have exactly one owner along every execution path. Double
consumption, use-after-measure, and leaks are typed failures, not runtime
surprises.
4. Codes, profiles, encodings — three different things
A
Codeis validated algebra: a physical widthn, a logical widthk, and independent stabilizer and logical-operator bases, all checked at construction. Distance is evidence, held as acudaq.logical.codes.Distance: a bare integer normalizes toclaimed— a recorded assertion, never a proof — while the evidence-bearing constructors (exact,lower_bound,upper_bound,circuit) require a method and provenance.A
CodeProfileis an analysis convention over one code: effective syndrome generators, meta-checks, and derived boundary maps. Change the convention and you get a new profile, not a new code.An
Encodingis a reusable logical view: named logical ports, a block ABI name, and layout facts. Preparation and conversion are gadgets — an encoding never executes circuits.
Every code synthesizes a default profile and encoding, so you write your own
only when you need a genuinely different view. The catalog
(cudaq.logical.codes) ships Steane, Repetition,
rotated_surface(distance), ReedMuller15, and BareQubit:
import cudaq.logical as cql
steane = cql.codes.Steane # the [[7,1,3]] CSS code
assert (steane.n, steane.k) == (7, 1)
assert (steane.d.value, steane.d.status) == (3, "claimed")
The two structures most worth keeping apart, side by side:
Structure |
Holds |
|---|---|
|
physical width |
|
the protected |
5. Objectives, gadgets, protocols — claims and proofs
A gadget is a realization — a typed circuit over encoded patches — plus a
claim: implements=<objective>. That claim is checked. The compiler derives the
gadget’s signed symplectic action, matches it against the objective’s Clifford
action, and records the equivalence evidence — or fails with a typed diagnostic:
import cudaq.logical as cql
@cql.objective
def terminal_memory(q: cql.types.logical_qubit) -> None:
cql.discard(q)
@cql.gadget(implements=terminal_memory)
def steane_memory(block: cql.patch[cql.codes.Steane]) -> None:
block, _ = cql.extract_syndrome(block)
block, _ = cql.mz(block.data)
cql.discard(block)
build = cql.compile(steane_memory)
assert build.stage == cql.stages.P2
assert build.facets == (cql.stages.Facet.QEC_SPEC, cql.stages.Facet.QEC_REALIZATION)
When several operand-to-port embeddings verify, the compiler refuses to pick one
silently and raises cudaq.logical.errors.AmbiguousLogicalPortMap. An explicit
logical_ports= mapping is a constraint the verifier checks, never evidence it
trusts.
A protocol composes gadget calls with operational policy. The shipped
15-to-1 distillation in examples/standalone/03_magic_state_distillation.py
exercises typed resource requests and postselection. Protocols may also use
bounded retry with explicit commit points; retry policy is normalized at
construction into immutable, type-checked structures:
Type |
Fields |
|---|---|
|
positive |
|
|
6. Evidence follows the program
Every semantic transition emits evidence records — pass, fail, or
unresolved — or an explicit declaration that evidence is missing.
build.evidence lists the records, and build.status summarizes the build
(root, stage, facets, and counts by result), as in the gadget build above.
build.serialize() captures the whole build — selected definitions, evidence,
and all — and cudaq.logical.compiler.Build.replay reopens it in a clean
process with no ambient Python state. The same honesty applies to distances and
estimates: a claimed distance stays a claim, and each estimation tier reports
exactly which facts it consumed.
7. There is no registry — imports are the linker
CUDA-Q Logical has no global implementation table, and that is deliberate. A registry would make import order part of your program’s semantics: two sessions that import modules in a different order could select different physics, and a serialized build could not say what was visible when it was compiled.
Instead, discovery is scoped and explicit. Each compilation collects candidates from exactly three places:
definitions bound at module scope in your program’s module;
definitions bound at module scope in your device’s module;
definitions exported by a
cudaq.logicallibrary submodule you explicitly imported into one of those modules — the import statement is the link act, and bareimport cudaq.logicallinks nothing.
Each compilation derives this candidate set fresh, filters it by objective and
boundary types, and captures the selected closure into the build:
build.source_modules and build.definitions record exactly what was visible
and what won. Day to day, you feel three consequences:
a gadget defined in a helper file you never imported is invisible — the failure is a typed “no feasible P2 implementation”, not a mystery winner;
deleting an import genuinely detaches its implementations;
replaying a build needs no ambient Python state at all.
8. Global phase is not observable
CUDA-Q Logical treats states and operators as projective: an overall phase is neither observable nor tracked. The only observables are measurement outcomes, and those are invariant under an overall phase. Concretely:
Synthesis targets a projective operator-norm bound (
cudaq.logical.compiler.synthesize(gate_set=..., precision=...)), soR_Z(kπ/4) = T^kholds up to phase.Rotations are 4π-periodic exactly and 2π-periodic up to phase.
cudaq.logical.algebra.Anglekeeps angles as exact rational multiples of π, so this stays precise — and the angle you authored is preserved, never auto-reduced:
import cudaq.logical as cql
assert cql.algebra.Angle(9, 4).pi_fraction == (9, 4)
assert float(cql.algebra.Angle(9, 4) - cql.algebra.Angle(1, 4)) == float(2 * cql.algebra.pi)
The boundary: dropping global phase is safe for a linear, classically conditioned program measured at the end. CUDA-Q Logical’s conditionals are classical — they condition on measurement outcomes — so no shipped surface needs to track phase. Quantum-controlled arbitrary unitaries would change that, and they are out of scope.
Where the pieces live
You write |
You get |
Canonical home |
|---|---|---|
|
portable P0 logical program |
your module |
|
logical machine for P1 placement |
|
|
validated |
|
|
verified realization / composition |
yours, |
|
a device layered across P1, P2, and P3 |
|
|
immutable, replayable |
|
|
a verified schedule over a P3 event graph |
|
|
logical, static, analytical, or schedule evidence |
|
|
compilation targets that lower a |
|
Naming follows PEP 8 throughout — artifact classes are camel case (Code,
Encoding), while operations, decorators, and constants are snake_case
(@cudaq.logical.machine, cudaq.logical.extract_syndrome). One deliberate
near-collision to know about: cudaq.logical.types.X(q) constructs a Pauli
factor for products, while cudaq.logical.x(q) applies the gate.
Where to go next
The architecture reference describes the dialect stack and pipeline presets that realize these concepts.
The use cases put them to work task by task.
Examples shows them in runnable, shipped code.