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

Tier.LOGICAL

a verified P0 program — no device, no code

LogicalProfile: action/instrument totals, peak logical qubits, idle/discard counts, action-depth upper bound, and synthesis demand

Tier.STATIC

a selected P2 build — code, gadgets, and protocols chosen

FabricCounts: per-operation counts, gadget/protocol call totals, resource requests, postselection bookkeeping, syndrome rounds, and peak patches

Tier.ANALYTICAL

a selected P2 build and physical operating assumptions

FabricEstimate: modeled error, acceptance, wall-clock cost, physical-qubit peak, assumptions, and retry demand

Tier.SCHEDULE

a scheduled P3 physical graph

ScheduleEstimate: authenticated event counts, makespan, resource-time, physical resources, utilization, and termination behavior

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.repeat trip 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_root and build_sha256 it 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 assumptions list and an evidence citation to every emitted qlx.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.LOGICAL requires P0, Tier.STATIC and Tier.ANALYTICAL require a selected P2 build, and Tier.SCHEDULE requires 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.analysis provides the provenance spellings: citation(...) for a published source, report(...) and computation(...) for internal analyses and recorded tool runs, and user_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