Resource estimation
Estimation in CUDA-Q Logical comes in four explicit tiers over the same
linked definitions. One call shape serves all four:
cudaq.logical.estimate(value, tier=...). You can pass a Build, schedule, or
authoring definition. A definition is first compiled through its normal default
pipeline; the estimator then checks that the resulting stage matches the
requested tier and fails with a typed diagnostic when they disagree.
The tier names and result types below live in cudaq.logical.estimate,
written here without that prefix:
Tier |
Needs |
Returns |
|---|---|---|
|
a verified P0 program — no device, no code |
|
|
a selected P2 build — code, gadgets, and protocols chosen |
|
|
a selected P2 build and physical operating assumptions |
|
|
a scheduled P3 physical graph |
|
Tier.STATIC is the default, so cudaq.logical.estimate(p2_build) needs no
tier= argument. Results are immutable typed values, expose plain-data
projections where applicable, and retain the source identity and evidence used
to derive them.
Tier.LOGICAL — cost the algorithm before any QEC choice
The logical tier works with no device and no code at all. You can estimate the
algorithm while it is still portable intent
(examples/standalone/00_logical_program.py):
import cudaq.logical as cql
@cql.program
def bell() -> tuple[bool, bool]:
q = cql.allocate(2, state=cql.types.zero)
q[0] = cql.h(q[0])
q[0], q[1] = cql.cx(q[0], q[1])
return cql.measure_z(q[0]), cql.measure_z(q[1])
profile = cql.estimate(cql.compile(bell), tier=cql.estimate.Tier.LOGICAL)
assert profile.logical_qubits_peak == 2
assert profile.actions == {"qlx_standard_h": 1, "qlx_standard_cx": 1}
assert profile.instruments == {
"qlx_standard_prepare_zero": 2,
"qlx_standard_measure_z": 2,
}
Non-Clifford standard actions (T, T†, CCZ) show up in actions like
everything else, and additionally in profile.synthesis_demand. That way you
can see the demand a Clifford+T synthesis pass will have to meet before you
commit to any gate set (exercised in
python/tests/cudaq/logical/test_quake_import.py).
Tier.STATIC — count one selected P2 realization
Once you have selected codes, gadgets, and protocols, the static tier walks
the executable Fabric closure and counts what would actually run. Here is
the standalone Steane terminal-memory gadget
(examples/standalone/02_code_and_gadget.py):
counts = cql.estimate(gadget_build, tier=cql.estimate.Tier.STATIC)
assert counts.patches_peak == 1
assert counts.build_root == "steane_memory"
assert counts.source_stage == "p2"
counts.operation_counts
# {'reset': 2, 'h': 2, 'cx': 2, 'read_syndrome_ancillas': 1, 'mz': 1, 'dealloc': 1}
The syndrome-extraction gadget has been lowered to its physical primitives, so
the counts report the reset/H/CX/measurement work of the actual circuit —
not the one-line cudaq.logical.extract_syndrome the author wrote.
Protocols compose gadgets with resources and postselection, and the static tier keeps the bookkeeping visible. You can estimate the standalone 15-to-1 distillation example straight from its authoring definition:
counts = cql.estimate(distill_15to1, tier=cql.estimate.Tier.STATIC)
assert counts.operation_counts["resource_request"] == 15
assert counts.operation_counts["selection"] == 4
assert counts.operation_counts["pack_resource"] == 1
The operation counts record how many zero-on-accept selection checks guard the result, and resource-request operations count everything the protocol asks for — including attempts that postselection may discard. Selection is never averaged away silently.
Tier.ANALYTICAL — apply an explicit physical model
The analytical tier starts from a selected P2 operation network and combines its static counts with a physical error rate, failure budget, scaling law, and cycle time. A device operating point can supply these values, and callers can override them for sensitivity studies. The result keeps its assumptions and reports whether the selected distance meets the requested budget.
examples/02_surface_code_resource_estimate.py uses this tier through a CUDA-Q
target, while examples/standalone/05_gidney_ekera_lookup_addition.py invokes
it directly on a P2 build.
Tier.SCHEDULE — cost the physical event graph
The schedule tier consumes a scheduled P3 graph. It reports authenticated
physical-resource and event counts, first-attempt/expected/maximum makespans,
resource-time, utilization, exhaustion probability, and the selected
termination policy. examples/standalone/04_physical_schedule.py shows the
smallest direct workflow; the surface-code and Carbon CUDA-Q targets expose the
same tier through cudaq.estimate annotations.
The rules that keep estimates honest
Folding. Loops and multiplicities stay symbolic in the IR; the estimator applies a
fabric.repeattrip count or a call multiplicity as a numeric multiplier instead of unrolling. A long memory experiment costs the same to estimate as a short one.Provenance. Every estimate records the
build_rootandbuild_sha256it refined, plus the source stage and facets. Estimators derive counts from a freshly re-verified replay of the build’s frozen snapshot — never from the mutable module view exposed for inspection.Assumptions in the open. The native estimation passes attach an explicit
assumptionslist and anevidencecitation to every emittedqlx.estimate_result(for example,"dynamic branches are counted as a static upper bound"), so an estimate states next to its numbers what it assumed and what it counted.Fail-closed gating.
Tier.LOGICALrequires P0,Tier.STATICandTier.ANALYTICALrequire a selected P2 build, andTier.SCHEDULErequires a scheduled P3 graph. A stage/tier mismatch, missing operating assumption, or unverifiable module is a typed error, never a partially computed number.Declared assumptions. Where a fact is evidence rather than algebra — a code distance, say — the typed evidence constructors ask for a method and a provenance, and
cudaq.logical.analysisprovides the provenance spellings:citation(...)for a published source,report(...)andcomputation(...)for internal analyses and recorded tool runs, anduser_assertion(...)for an explicit, unproved statement.
Direct spellings
cudaq.logical.analysis.logical_counts(p0),
cudaq.logical.analysis.count(build),
cudaq.logical.estimate.analytical(build, ...), and
cudaq.logical.estimate.scheduled(schedule, ...) are the per-tier function
forms of the same estimators. Reach for them when your code intentionally
selects one specialized analysis; product flows should prefer the unified
cudaq.logical.estimate(...) front door.
Sweeps and reproducibility
Design-space sweeps are first-class artifacts.
cudaq.logical.compiler.compile_many(points, pipeline=...) turns a tuple of
cudaq.logical.compiler.Experiment values into an immutable, self-describing
ExperimentBundle. Its serialized form replays every build bit-identically in
a clean process (python/tests/cudaq/logical/test_experiments.py):
import cudaq.logical as cql
@cql.program
def memory() -> bool:
q = cql.prepare_zero()
q = cql.idle(q, rounds=3)
return cql.measure_z(q)
points = tuple(
cql.compiler.Experiment(root=memory, parameters={"p": p})
for p in (1e-4, 1e-3, 1e-2))
bundle = cql.compiler.compile_many(
points, pipeline=cql.compiler.pipelines.logical())
assert len(bundle) == 3
replayed = cql.compiler.ExperimentBundle.replay(bundle.serialize())
A serialized bundle needs no ambient Python state, so you can re-derive an estimate quoted in a paper from the bundle alone.
Paper-specific projections live in the open
The built-in analytical tier is not a substitute for a paper-specific system
model. examples/05_gidney_ekera.py compiles a windowed-arithmetic RSA-2048
resource kernel to a folded logical profile, then applies the paper’s explicit
timing and layout equations. That default fast path deliberately does not call
Tier.ANALYTICAL; its assumptions remain visible and editable in the example.
The same example offers --physical as an opt-in P3 compilation and scheduling
path. The related library calculation is available through
cudaq.logical.algorithms.estimate_gidney_ekera.
Estimating ordinary CUDA-Q kernels
When you select a cudaq.logical target, ordinary CUDA-Q kernels compile
through the stages that target owns. The logical, Clifford+T, Fermi–Hubbard,
surface-code, and Carbon examples exercise progressively deeper target stacks.
cudaq.estimate(kernel) returns the available per-tier results as CUDA-Q
annotations, and typed views rehydrate directly from them:
from cudaq.logical.estimate import FabricCounts, LogicalEstimate
estimates = cudaq.estimate(kernel, kernel_args)
static = FabricCounts.from_annotations(estimates.annotations)
logical = LogicalEstimate.from_annotations(estimates.annotations)
assert static.patches_peak == 1
assert logical.logical_qubits_peak == 1