Source code for qarp.resources._counting

"""Pure counting: commands / CircuitDAG → :class:`ResourceVector`.

Everything here is *counted* from the circuit as it exists — no cost models
(those live in :mod:`qarp.resources`).  Built on the existing C++
primitives ``CircuitDAG.count_ops()`` / ``depth()`` plus a per-command walk,
which is exact for variable-arity gates (``MCZ``, ``Custom``) where the
static gate table only records a minimum arity — the walk reads each
command's own qubit list, so a width-6 ``MCZ`` lands in ``n_3q_plus``
rather than being mis-bucketed by its table minimum of 2.

This module may import only ``qarpx`` and :mod:`qarp.resources`.
"""

from functools import lru_cache
from typing import Sequence

import qarpx as qx

from ._vector import Provenance, ResourceVector, Stage

# Non-physical commands (qx.gate_is_physical is the single classification —
# shared with Block.n_nqb_gates) minus Measure/Reset, which get their own
# counters; the remainder is excluded from n_gates and op_histogram.
_PSEUDO = frozenset(
    qx.gate_name(gt) for gt in qx.GateType.__members__.values() if not qx.gate_is_physical(gt)
) - {"Measure", "Reset"}
_MEASURE = "Measure"
_RESET = "Reset"
_OPAQUE = "Custom"  # unknown unitary: forces t_count to None
_MCZ = "MCZ"
_T_GATES = frozenset({"T", "Tdg"})


@lru_cache(maxsize=1)
def _gate_table() -> dict[str, tuple[int, int]]:
    """gate name → (num_qubits, num_params) for every bound GateType."""
    return {
        qx.gate_name(gt): (qx.gate_num_qubits(gt), qx.gate_num_params(gt))
        for gt in qx.GateType.__members__.values()
    }


[docs] def count_resources( source: "Sequence[qx.Command] | qx.CircuitDAG", *, provenance: Provenance, n_qubits: int | None = None, ) -> ResourceVector: """Count a circuit snapshot into a :class:`ResourceVector`. ``n_qubits`` overrides the inferred width (needed for idle qubits: the DAG infers max-wire+1, a block or device may be wider). ``swap_count`` is populated only when ``provenance.stage is Stage.ROUTED``; ``t_count`` only when no parametric, opaque, or width >= 3 ``MCZ`` unitary remains. Gates inside branch regions count unconditionally (worst-case). """ if isinstance(source, qx.CircuitDAG): dag = source cmds = dag.to_commands() else: cmds = list(source) dag = qx.CircuitDAG.from_commands(cmds) table = _gate_table() n_gates = n_1q = n_2q = n_3q_plus = n_meas = n_resets = t_count = 0 exact_t = True for cmd in cmds: name = qx.gate_name(cmd.gate) if name in _PSEUDO: continue if name == _MEASURE: n_meas += 1 continue if name == _RESET: n_resets += 1 continue n_gates += 1 arity = len(cmd.qubits) if arity == 1: n_1q += 1 elif arity == 2: n_2q += 1 else: n_3q_plus += 1 if name in _T_GATES: t_count += 1 if name == _OPAQUE or table[name][1] > 0: exact_t = False # A width >= 3 MCZ has no T-free exact form: like a parametric Rz, # its T-cost is unknown until lowered or synthesized (§19). Width 2 # (= CZ) is Clifford and stays countable. if name == _MCZ and arity >= 3: exact_t = False histogram = {k: v for k, v in dag.count_ops().items() if k not in _PSEUDO} return ResourceVector( n_qubits=dag.n_qubits if n_qubits is None else n_qubits, depth=dag.depth(), n_gates=n_gates, n_1q=n_1q, n_2q=n_2q, n_3q_plus=n_3q_plus, n_measurements=n_meas, n_resets=n_resets, t_count=t_count if exact_t else None, swap_count=(histogram.get("SWAP", 0) if provenance.stage is Stage.ROUTED else None), op_histogram=histogram, provenance=provenance, )