diff --git a/qualtran/bloqs/basic_gates/hadamard.py b/qualtran/bloqs/basic_gates/hadamard.py index c3fd0d791..4457a0c52 100644 --- a/qualtran/bloqs/basic_gates/hadamard.py +++ b/qualtran/bloqs/basic_gates/hadamard.py @@ -174,23 +174,15 @@ def as_cirq_op( } def _t_complexity_(self) -> 'TComplexity': - # This is based on the decomposition provided by `cirq.decompose_multi_controlled_rotation` - # which uses three cirq.MatrixGate's to do a controlled version of any single-qubit gate. - # The first MatrixGate happens to be a clifford, Hadamard operation in this case. - # The other two are considered 'rotations'. - # https://github.com/quantumlib/Qualtran/issues/237 - return TComplexity(rotations=2, clifford=4) + # https://github.com/quantumlib/Qualtran/issues/878#issuecomment-2257237443 + return TComplexity(t=2, clifford=9) def my_static_costs(self, cost_key: 'CostKey'): from qualtran.resource_counting import GateCounts, QECGatesCost if cost_key == QECGatesCost(): - # This is based on the decomposition provided by `cirq.decompose_multi_controlled_rotation` - # which uses three cirq.MatrixGate's to do a controlled version of any single-qubit gate. - # The first MatrixGate happens to be a clifford, Hadamard operation in this case. - # The other two are considered 'rotations'. - # https://github.com/quantumlib/Qualtran/issues/237 - return GateCounts(rotation=2, clifford=4) + # https://github.com/quantumlib/Qualtran/issues/878#issuecomment-2257237443 + return GateCounts(t=2, clifford=9) return NotImplemented diff --git a/qualtran/bloqs/chemistry/sparse/sparse_test.py b/qualtran/bloqs/chemistry/sparse/sparse_test.py index e910f3455..b79e141ff 100644 --- a/qualtran/bloqs/chemistry/sparse/sparse_test.py +++ b/qualtran/bloqs/chemistry/sparse/sparse_test.py @@ -68,10 +68,9 @@ def qrom_cost(prep: PrepareSparse) -> int: def get_toffoli_count(bloq: Bloq) -> SymbolicInt: - """Get the toffoli count of a bloq assuming no raw Ts.""" + """Get the toffoli count of a bloq ignoring raw Ts/Rotations.""" counts = get_cost_value(bloq, QECGatesCost(), generalizer=generalize_cswap_approx) - cost_dict = counts.total_t_and_ccz_count(ts_per_rotation=0) - assert cost_dict['n_t'] == 0 + cost_dict = counts.total_t_and_ccz_count() return cost_dict['n_ccz'] diff --git a/qualtran/cirq_interop/t_complexity_protocol.py b/qualtran/cirq_interop/t_complexity_protocol.py index 6e96057fd..e7da52a24 100644 --- a/qualtran/cirq_interop/t_complexity_protocol.py +++ b/qualtran/cirq_interop/t_complexity_protocol.py @@ -20,7 +20,7 @@ from qualtran import Bloq, DecomposeNotImplementedError, DecomposeTypeError from qualtran.resource_counting import SympySymbolAllocator -from qualtran.symbolics import ceil, log2, SymbolicFloat, SymbolicInt +from qualtran.symbolics import ceil, SymbolicFloat, SymbolicInt from .decompose_protocol import _decompose_once_considering_known_decomposition @@ -38,7 +38,9 @@ class TComplexity: @staticmethod def rotation_cost(eps: SymbolicFloat) -> SymbolicFloat: - return ceil(1.149 * log2(1.0 / eps) + 9.2) + from qualtran.resource_counting import GateCounts + + return GateCounts.rotation_t_cost(eps) def t_incl_rotations(self, eps: float = 1e-11) -> SymbolicInt: """Return the total number of T gates after compiling rotations""" diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index 95cf98d69..46b6e3076 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -12,15 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from collections import defaultdict -from typing import Callable, Dict, Sequence, Tuple, TYPE_CHECKING +from collections import Counter, defaultdict +from typing import Callable, Dict, Iterator, Mapping, Sequence, Tuple, TYPE_CHECKING, Union import attrs import networkx as nx +import numpy as np import sympy from attrs import field, frozen -from qualtran.symbolics import SymbolicInt +from qualtran.symbolics import ceil, is_symbolic, log2, ssum, SymbolicFloat, SymbolicInt from ._call_graph import get_bloq_callee_counts from ._costing import CostKey @@ -120,12 +121,36 @@ def __str__(self): return f'{self.gateset_name} counts' +FloatReprT = Union[str, sympy.Expr] +"""The type to represent floats as, to use as safe keys in mappings.""" + + +def _mapping_to_counter(mapping: Mapping[FloatReprT, int]) -> Counter[FloatReprT]: + if isinstance(mapping, Counter): + return mapping + return Counter(mapping) + + @frozen(kw_only=True) class GateCounts: """A data class of counts of the typical target gates in a compilation. Specifically, this class holds counts for the number of `TGate` (and adjoint), `Toffoli`, `TwoBitCSwap`, `And`, clifford bloqs, single qubit rotations, and measurements. + + Attributes: + t: number of `TGate` (and adjoint). + toffoli: number of `Toffoli`. + cswap: number of `TwoBitCSwap`. + and_bloq: number of `And`. + clifford: number of clifford bloqs. + measurement: number of 1-qubit measurements. + binned_rotation_epsilons: A Counter of rotation precision (epsilon) to the number of + rotations with that particular epsilon. The `epsilon` (used as the mapping key) + is formatted into a string using `np.format_float_scientific` for concrete values, + and stored as-is for symbolic values. + See constructor `from_rotation_with_eps` to construct a `GateCounts` object + given a precision `epsilon`. """ t: SymbolicInt = 0 @@ -133,8 +158,40 @@ class GateCounts: cswap: SymbolicInt = 0 and_bloq: SymbolicInt = 0 clifford: SymbolicInt = 0 - rotation: SymbolicInt = 0 measurement: SymbolicInt = 0 + binned_rotation_epsilons: Counter[FloatReprT] = field( + factory=Counter, converter=_mapping_to_counter, eq=lambda d: tuple(d.items()) + ) + + @classmethod + def from_rotation_with_eps( + cls, eps: SymbolicFloat, *, n_rotations: int = 1, eps_repr_prec: int = 10 + ): + """Construct a GateCount with a rotation of precision `eps`. + + Formats the value of `eps` as a string using `np.format_float_scientific`, + to use as a safe dictionary key. If `eps` is symbolic, it is used as-is. + + Args: + eps: precision to synthesize the rotation(s). + eps_repr_prec: number of digits to approximate `eps` to. Uses 10 by default. + See `np.format_float_scientific` for more details. + If `eps` is symbolic, this parameter is ignored. + n_rotations: number of rotations, defaults to 1. + """ + if is_symbolic(eps): + eps_bin: FloatReprT = eps + else: + eps_bin = np.format_float_scientific( + eps, precision=eps_repr_prec, min_digits=eps_repr_prec + ) + return cls(binned_rotation_epsilons=Counter({eps_bin: n_rotations})) + + def iter_rotations_with_epsilon(self) -> Iterator[tuple[SymbolicFloat, SymbolicInt]]: + """Iterate through the rotation precisions (epsilon) and their frequency.""" + for eps_bin, n_rot in self.binned_rotation_epsilons.items(): + eps: SymbolicFloat = eps_bin if is_symbolic(eps_bin) else float(eps_bin) + yield eps, n_rot def __add__(self, other): if not isinstance(other, GateCounts): @@ -146,19 +203,22 @@ def __add__(self, other): cswap=self.cswap + other.cswap, and_bloq=self.and_bloq + other.and_bloq, clifford=self.clifford + other.clifford, - rotation=self.rotation + other.rotation, measurement=self.measurement + other.measurement, + binned_rotation_epsilons=self.binned_rotation_epsilons + other.binned_rotation_epsilons, ) def __mul__(self, other): + """Multiplies the frequency of each operation with `other`.""" return GateCounts( t=other * self.t, toffoli=other * self.toffoli, cswap=other * self.cswap, and_bloq=other * self.and_bloq, clifford=other * self.clifford, - rotation=other * self.rotation, measurement=other * self.measurement, + binned_rotation_epsilons=Counter( + {eps_bin: other * n_rot for eps_bin, n_rot in self.binned_rotation_epsilons.items()} + ), ) def __rmul__(self, other): @@ -179,36 +239,56 @@ def _is_nonzero(v): return True return maybe_nonzero - return {k: v for k, v in d.items() if _is_nonzero(v)} + def _keep(key, value) -> bool: + if key == 'binned_rotation_epsilons': + return value + return _is_nonzero(value) + + return {k: v for k, v in d.items() if _keep(k, v)} + + @staticmethod + def rotation_t_cost(eps: SymbolicFloat) -> SymbolicInt: + """T-cost of a single Z rotation with precision `eps`. + + References: + [Efficient synthesis of universal Repeat-Until-Success circuits](https://arxiv.org/abs/1404.5320) + Bocharov et. al. 2014. Page 4, Paragraph "Simulation Results." + """ + return ceil(1.149 * log2(1.0 / eps) + 9.2) + + def total_rotations_as_t(self) -> SymbolicInt: + """Total number of T Gates for the rotations.""" + return ssum( + n_rotations * self.rotation_t_cost(eps) + for eps, n_rotations in self.iter_rotations_with_epsilon() + ) def total_t_count( - self, - ts_per_toffoli: int = 4, - ts_per_cswap: int = 7, - ts_per_and_bloq: int = 4, - ts_per_rotation: int = 11, + self, ts_per_toffoli: int = 4, ts_per_cswap: int = 7, ts_per_and_bloq: int = 4 ) -> int: """Get the total number of T Gates for the `GateCounts` object. This simply multiplies each gate type by its cost in terms of T gates, which is configurable via the arguments to this method. - - The default value for `ts_per_rotation` assumes the rotation is approximated using - `Mixed fallback` protocol with error budget 1e-3. """ return ( self.t + ts_per_toffoli * self.toffoli + ts_per_cswap * self.cswap + ts_per_and_bloq * self.and_bloq - + ts_per_rotation * self.rotation + + self.total_rotations_as_t() ) - def total_t_and_ccz_count(self, ts_per_rotation: int = 11) -> Dict[str, SymbolicInt]: + def total_t_and_ccz_count(self) -> Dict[str, SymbolicInt]: n_ccz = self.toffoli + self.cswap + self.and_bloq - n_t = self.t + ts_per_rotation * self.rotation + n_t = self.t + self.total_rotations_as_t() return {'n_t': n_t, 'n_ccz': n_ccz} + @property + def rotations_ignoring_eps(self) -> SymbolicInt: + """Total number of rotations, ignoring the individual precisions.""" + return ssum(self.binned_rotation_epsilons.values()) + def total_beverland_count(self) -> Dict[str, SymbolicInt]: r"""Counts used by Beverland. et. al. using notation from the reference. @@ -221,17 +301,20 @@ def total_beverland_count(self) -> Dict[str, SymbolicInt]: Toffoli gates. Since we don't compile the 'layers' explicitly, we set this to be the number of rotations. + Note: This costing method ignores the individual rotation precisions (`eps`). + Reference: https://arxiv.org/abs/2211.07629. Equation D3. """ toffoli = self.toffoli + self.and_bloq + self.cswap + rotation = self.rotations_ignoring_eps return { 'meas': self.measurement, - 'R': self.rotation, + 'R': rotation, 'T': self.t, 'Tof': toffoli, - 'D_R': self.rotation, + 'D_R': rotation, } @@ -283,7 +366,8 @@ def compute(self, bloq: 'Bloq', get_callee_cost: Callable[['Bloq'], GateCounts]) return GateCounts() if bloq_is_rotation(bloq): - return GateCounts(rotation=1) + assert hasattr(bloq, 'eps') + return GateCounts.from_rotation_with_eps(bloq.eps) # Recursive case totals = GateCounts() diff --git a/qualtran/resource_counting/_bloq_counts_test.py b/qualtran/resource_counting/_bloq_counts_test.py index 6688459a6..8bdd8a694 100644 --- a/qualtran/resource_counting/_bloq_counts_test.py +++ b/qualtran/resource_counting/_bloq_counts_test.py @@ -19,6 +19,7 @@ from qualtran.bloqs.basic_gates._shims import Measure from qualtran.bloqs.for_testing.costing import make_example_costing_bloqs from qualtran.resource_counting import BloqCount, GateCounts, get_cost_value, QECGatesCost +from qualtran.symbolics import ceil, log2 def test_bloq_count(): @@ -60,6 +61,24 @@ def test_gate_counts(): assert str(gc2) == 't: n, cswap: 2' +def test_gate_counts_rotations(): + gc = GateCounts.from_rotation_with_eps(1e-10, n_rotations=4) + assert gc == GateCounts(binned_rotation_epsilons={'1.0000000000e-10': 4}) + + eps = sympy.Symbol(r"\epsilon") + gc_symb = GateCounts.from_rotation_with_eps(eps, n_rotations=6) + assert gc_symb == GateCounts(binned_rotation_epsilons={eps: 6}) + + +def test_gate_counts_rotations_to_t(): + gc = GateCounts.from_rotation_with_eps(1e-10, n_rotations=4) + assert gc.total_rotations_as_t() == 192 + + eps = sympy.Symbol(r"\epsilon") + gc_symb = GateCounts.from_rotation_with_eps(eps, n_rotations=6) + assert gc_symb.total_rotations_as_t() == 6 * ceil(1.149 * log2(1.0 / eps) + 9.2) + + def test_qec_gates_cost(): algo = make_example_costing_bloqs() gc = get_cost_value(algo, QECGatesCost()) @@ -80,12 +99,15 @@ def test_qec_gates_cost(): # And [mcmt.And(), GateCounts(and_bloq=1)], # Rotations - [basic_gates.ZPowGate(exponent=0.1, global_shift=0.0, eps=1e-11), GateCounts(rotation=1)], + [ + basic_gates.ZPowGate(exponent=0.1, global_shift=0.0, eps=1e-11), + GateCounts.from_rotation_with_eps(1e-11), + ], [ rotations.phase_gradient.PhaseGradientUnitary( bitsize=10, exponent=1, is_controlled=False, eps=1e-10 ), - GateCounts(clifford=2, t=1, rotation=7), + GateCounts(clifford=2, t=1) + GateCounts.from_rotation_with_eps(1e-11, n_rotations=7), ], # Recursive [mcmt.MultiControlX(cvs=(1, 1, 1)), GateCounts(and_bloq=2, measurement=2, clifford=3)], diff --git a/qualtran/surface_code/algorithm_summary_test.py b/qualtran/surface_code/algorithm_summary_test.py index 61b475665..fb84a4632 100644 --- a/qualtran/surface_code/algorithm_summary_test.py +++ b/qualtran/surface_code/algorithm_summary_test.py @@ -37,14 +37,18 @@ [mcmt.And(), AlgorithmSummary(n_algo_qubits=3, n_logical_gates=GateCounts(and_bloq=1))], [ basic_gates.ZPowGate(exponent=0.1, global_shift=0.0, eps=1e-11), - AlgorithmSummary(n_algo_qubits=1, n_logical_gates=GateCounts(rotation=1)), + AlgorithmSummary( + n_algo_qubits=1, n_logical_gates=GateCounts.from_rotation_with_eps(1e-11) + ), ], [ rotations.phase_gradient.PhaseGradientUnitary( bitsize=10, exponent=1, is_controlled=False, eps=1e-10 ), AlgorithmSummary( - n_algo_qubits=10, n_logical_gates=GateCounts(clifford=2, t=1, rotation=7) + n_algo_qubits=10, + n_logical_gates=GateCounts(clifford=2, t=1) + + GateCounts.from_rotation_with_eps(1e-11, n_rotations=10), ), ], [ diff --git a/qualtran/surface_code/beverland_et_al_model.ipynb b/qualtran/surface_code/beverland_et_al_model.ipynb index a3c6f95f5..39991e12d 100644 --- a/qualtran/surface_code/beverland_et_al_model.ipynb +++ b/qualtran/surface_code/beverland_et_al_model.ipynb @@ -208,11 +208,13 @@ "source": [ "qd_alg = AlgorithmSummary(\n", " n_algo_qubits = 100,\n", - " n_logical_gates = GateCounts(\n", - " rotation=30_100,\n", - " # Note in the paper the number of measurements\n", - " # has an extra zero which we assume to be a typo.\n", - " measurement=1.4e5,\n", + " n_logical_gates = (\n", + " GateCounts.from_rotation_with_eps(0, n_rotations=30_100)\n", + " + GateCounts(\n", + " # Note in the paper the number of measurements\n", + " # has an extra zero which we assume to be a typo.\n", + " measurement=int(1.4e5)\n", + " )\n", " ),\n", " n_rotation_layers = 501\n", ")" @@ -394,11 +396,10 @@ "chem_alg = AlgorithmSummary(\n", " n_algo_qubits=1318,\n", " n_logical_gates=GateCounts(\n", - " rotation=2.06e8,\n", " measurement=1.37e9,\n", " toffoli=1.35e11,\n", " t=5.53e7,\n", - " ),\n", + " ) + GateCounts.from_rotation_with_eps(0, n_rotations=2.06e8),\n", " n_rotation_layers=2.05e8,\n", ")\n", "chem_alg" @@ -569,13 +570,12 @@ "shor_alg = AlgorithmSummary(\n", " n_algo_qubits=12581,\n", " n_logical_gates=GateCounts(\n", - " rotation=12,\n", " measurement=1.08e9,\n", " # Note in the paper the number of Toffoli operations is 3.73e10.\n", " # However we assume that the exponent has a typo and that the number is 3.73e9.\n", " toffoli=3.73e9,\n", " t=12,\n", - " ),\n", + " ) + GateCounts.from_rotation_with_eps(0, n_rotations=12),\n", " n_rotation_layers=12,\n", ")" ] diff --git a/qualtran/surface_code/beverland_et_al_model.py b/qualtran/surface_code/beverland_et_al_model.py index 7d57b1670..454bcd8fb 100644 --- a/qualtran/surface_code/beverland_et_al_model.py +++ b/qualtran/surface_code/beverland_et_al_model.py @@ -117,8 +117,8 @@ def n_discrete_logical_gates( rotation_model: Cost model used to compute the number of T gates needed to approximate rotations. """ - n_rotations: SymbolicInt = alg.n_logical_gates.rotation - ret = attrs.evolve(alg.n_logical_gates, rotation=0) + n_rotations: SymbolicInt = alg.n_logical_gates.rotations_ignoring_eps + ret = attrs.evolve(alg.n_logical_gates, binned_rotation_epsilons={}) if n_rotations > 0: ret = ( ret diff --git a/qualtran/surface_code/beverland_et_al_model_test.py b/qualtran/surface_code/beverland_et_al_model_test.py index 7cf6c5eb2..c1d20a2f9 100644 --- a/qualtran/surface_code/beverland_et_al_model_test.py +++ b/qualtran/surface_code/beverland_et_al_model_test.py @@ -38,7 +38,10 @@ class Test: Test( alg=AlgorithmSummary( n_algo_qubits=100, - n_logical_gates=GateCounts(rotation=30_100, measurement=int(1.4e6)), + n_logical_gates=( + GateCounts.from_rotation_with_eps(0, n_rotations=30_000) + + GateCounts(measurement=int(1.4e6)) + ), n_rotation_layers=501, ), error_budget=1e-3, @@ -50,8 +53,9 @@ class Test: Test( alg=AlgorithmSummary( n_algo_qubits=1318, - n_logical_gates=GateCounts( - t=int(5.53e7), rotation=int(2.06e8), toffoli=int(1.35e11), measurement=int(1.37e9) + n_logical_gates=( + GateCounts(t=int(5.53e7), toffoli=int(1.35e11), measurement=int(1.37e9)) + + GateCounts.from_rotation_with_eps(0, n_rotations=int(2.06e8)) ), n_rotation_layers=int(2.05e8), ), @@ -64,8 +68,9 @@ class Test: Test( alg=AlgorithmSummary( n_algo_qubits=12581, - n_logical_gates=GateCounts( - t=12, rotation=12, toffoli=int(3.73e9), measurement=int(1.08e9) + n_logical_gates=( + GateCounts(t=12, toffoli=int(3.73e9), measurement=int(1.08e9)) + + GateCounts.from_rotation_with_eps(0, n_rotations=12) ), n_rotation_layers=12, ), @@ -106,5 +111,6 @@ def test_t_states(test: Test): assert got == pytest.approx(test.t_states, rel=0.1) +@pytest.mark.notebook def test_notebook(): qlt_testing.execute_notebook('beverland_et_al_model') diff --git a/qualtran/surface_code/ui.py b/qualtran/surface_code/ui.py index 85f4010c4..4c1d7e29f 100644 --- a/qualtran/surface_code/ui.py +++ b/qualtran/surface_code/ui.py @@ -522,7 +522,7 @@ def update( qec_name: str, magic_name: str, magic_count: int, - rotaion_model_name: str, + rotation_model_name: str, ): """Updates the visualization.""" if any(x is None for x in [physical_error_rate, error_budget, *algorithm_data, magic_count]): @@ -530,16 +530,20 @@ def update( # TODO: We implicitly rely on the order of the input components qubits, measurements, ts, toffolis, rotations, n_rotation_layers = algorithm_data + + rotation_eps = error_budget / rotations + algorithm = AlgorithmSummary( n_algo_qubits=qubits, - n_logical_gates=GateCounts( - measurement=measurements, t=ts, toffoli=toffolis, rotation=rotations + n_logical_gates=( + GateCounts(measurement=measurements, t=ts, toffoli=toffolis) + + GateCounts.from_rotation_with_eps(rotation_eps, n_rotations=rotations) ), n_rotation_layers=n_rotation_layers, ) qec = _QEC_SCHEMES[qec_name] magic_factory = _MAGIC_FACTORIES[magic_name] - rotation_model = _ROTATION_MODELS[rotaion_model_name] + rotation_model = _ROTATION_MODELS[rotation_model_name] n_logical_gates = beverland_et_al_model.n_discrete_logical_gates( eps_syn=error_budget / 3, alg=algorithm, rotation_model=rotation_model ) diff --git a/qualtran/surface_code/ui_test.py b/qualtran/surface_code/ui_test.py index 4fa2944a0..bc953f86c 100644 --- a/qualtran/surface_code/ui_test.py +++ b/qualtran/surface_code/ui_test.py @@ -29,7 +29,7 @@ def test_ensure_support_for_all_supported_models(estimation_model: str): qec_name='GidneyFowler', magic_name='FifteenToOne733', magic_count=1, - rotaion_model_name='BeverlandEtAlRotationCost', + rotation_model_name='BeverlandEtAlRotationCost', ) @@ -81,7 +81,7 @@ def test_update(estimation_model: str, desired): qec_name='GidneyFowler', magic_name='FifteenToOne733', magic_count=1, - rotaion_model_name='BeverlandEtAlRotationCost', + rotation_model_name='BeverlandEtAlRotationCost', ) assert ( display_runtime, @@ -104,7 +104,7 @@ def test_update_bad_input(): qec_name='GidneyFowler', magic_name='FifteenToOne733', magic_count=1, - rotaion_model_name='BeverlandEtAlRotationCost', + rotation_model_name='BeverlandEtAlRotationCost', )