From 8feb87011a7ba8bfa110c38b8e29a7df215934e1 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Tue, 6 Aug 2024 14:54:52 -0700 Subject: [PATCH 01/19] prototype: use rotation eps in GateCounts --- qualtran/resource_counting/_bloq_counts.py | 37 +++++++++++++++---- .../resource_counting/_bloq_counts_test.py | 7 +++- .../surface_code/algorithm_summary_test.py | 8 +++- .../surface_code/beverland_et_al_model.py | 14 +++---- .../beverland_et_al_model_test.py | 16 ++++++-- qualtran/surface_code/ui.py | 12 ++++-- qualtran/surface_code/ui_test.py | 6 +-- 7 files changed, 71 insertions(+), 29 deletions(-) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index 165e14bf1..6c48ea630 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -12,8 +12,8 @@ # 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, Mapping, Sequence, Tuple, TYPE_CHECKING import attrs import networkx as nx @@ -112,6 +112,12 @@ def __str__(self): return f'{self.gateset_name} counts' +def _mapping_to_counter(mapping: Mapping[float, int]) -> Counter[float]: + 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. @@ -125,8 +131,17 @@ class GateCounts: cswap: int = 0 and_bloq: int = 0 clifford: int = 0 - rotation: int = 0 measurement: int = 0 + rotation_epsilons: Counter[float] = field(factory=Counter, converter=_mapping_to_counter) + + @property + def rotation(self): + from qualtran.cirq_interop.t_complexity_protocol import TComplexity + + return sum( + n_rotations * int(TComplexity.rotation_cost(eps)) + for eps, n_rotations in self.rotation_epsilons.items() + ) def __add__(self, other): if not isinstance(other, GateCounts): @@ -138,8 +153,8 @@ 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, + rotation_epsilons=self.rotation_epsilons + other.rotation_epsilons, ) def __mul__(self, other): @@ -149,8 +164,8 @@ def __mul__(self, other): cswap=other * self.cswap, and_bloq=other * self.and_bloq, clifford=other * self.clifford, - rotation=other * self.rotation, measurement=other * self.measurement, + rotation_epsilons=Counter({k: other * v for k, v in self.rotation_epsilons.items()}), ) def __rmul__(self, other): @@ -167,7 +182,13 @@ def __str__(self): def asdict(self): d = attrs.asdict(self) - return {k: v for k, v in d.items() if v > 0} + + def _keep(key, value) -> bool: + if key == 'rotation_epsilons': + return value + return value > 0 + + return {k: v for k, v in d.items() if _keep(k, v)} def total_t_count( self, @@ -232,6 +253,7 @@ class QECGatesCost(CostKey[GateCounts]): def compute(self, bloq: 'Bloq', get_callee_cost: Callable[['Bloq'], GateCounts]) -> GateCounts: from qualtran.bloqs.basic_gates import TGate, Toffoli, TwoBitCSwap + from qualtran.bloqs.basic_gates.rotation import _HasEps from qualtran.bloqs.mcmt.and_bloq import And # T gates @@ -257,7 +279,8 @@ def compute(self, bloq: 'Bloq', get_callee_cost: Callable[['Bloq'], GateCounts]) return GateCounts(clifford=1) if bloq_is_rotation(bloq): - return GateCounts(rotation=1) + assert isinstance(bloq, _HasEps) + return GateCounts(rotation_epsilons={bloq.eps: 1}) # Recursive case totals = GateCounts() diff --git a/qualtran/resource_counting/_bloq_counts_test.py b/qualtran/resource_counting/_bloq_counts_test.py index 0bab46903..ae5a4c2d2 100644 --- a/qualtran/resource_counting/_bloq_counts_test.py +++ b/qualtran/resource_counting/_bloq_counts_test.py @@ -74,12 +74,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(rotation_epsilons={1e-11: 1}), + ], [ rotations.phase_gradient.PhaseGradientUnitary( bitsize=10, exponent=1, is_controlled=False, eps=1e-10 ), - GateCounts(rotation=10), + GateCounts(rotation_epsilons={1e-10: 10}), ], # Recursive [ diff --git a/qualtran/surface_code/algorithm_summary_test.py b/qualtran/surface_code/algorithm_summary_test.py index 5ec2280cf..59318f8a6 100644 --- a/qualtran/surface_code/algorithm_summary_test.py +++ b/qualtran/surface_code/algorithm_summary_test.py @@ -38,13 +38,17 @@ [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(rotation_epsilons={1e-11: 1}) + ), ], [ rotations.phase_gradient.PhaseGradientUnitary( bitsize=10, exponent=1, is_controlled=False, eps=1e-10 ), - AlgorithmSummary(n_algo_qubits=10, n_logical_gates=GateCounts(rotation=10)), + AlgorithmSummary( + n_algo_qubits=10, n_logical_gates=GateCounts(rotation_epsilons={1e-10: 10}) + ), ], [ mcmt.MultiControlPauli(cvs=(1, 1, 1), target_gate=cirq.X), diff --git a/qualtran/surface_code/beverland_et_al_model.py b/qualtran/surface_code/beverland_et_al_model.py index 1c9a3e2cc..b75287f09 100644 --- a/qualtran/surface_code/beverland_et_al_model.py +++ b/qualtran/surface_code/beverland_et_al_model.py @@ -116,14 +116,12 @@ def n_discrete_logical_gates( rotation_model: Cost model used to compute the number of T gates needed to approximate rotations. """ - n_rotations: int = alg.n_logical_gates.rotation - ret = attrs.evolve(alg.n_logical_gates, rotation=0) - if n_rotations > 0: - ret = ( - ret - + rotation_model.preparation_overhead(eps_syn) - + n_rotations * rotation_model.rotation_cost(eps_syn / n_rotations) - ) + rotation_epsilons: dict[float, int] = alg.n_logical_gates.rotation_epsilons + ret = attrs.evolve(alg.n_logical_gates, rotation_epsilons={}) + if rotation_epsilons: + rotation_model.preparation_overhead(min(eps for eps in rotation_epsilons.values())) + for eps, n_rotations in rotation_epsilons.items(): + ret += n_rotations * rotation_model.rotation_cost(eps) return 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..e054bc579 100644 --- a/qualtran/surface_code/beverland_et_al_model_test.py +++ b/qualtran/surface_code/beverland_et_al_model_test.py @@ -38,11 +38,13 @@ class Test: Test( alg=AlgorithmSummary( n_algo_qubits=100, - n_logical_gates=GateCounts(rotation=30_100, measurement=int(1.4e6)), + n_logical_gates=GateCounts( + rotation_epsilons={1e-3 / 30_000: 30_000}, measurement=int(1.4e6) + ), n_rotation_layers=501, ), error_budget=1e-3, - c_min=1.5e6, + c_min=2.5e6, time_steps=1.5e5, code_distance=9, t_states=602000, @@ -51,7 +53,10 @@ class 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) + t=int(5.53e7), + rotation_epsilons={1e-2 / 2.06e8: int(2.06e8)}, + toffoli=int(1.35e11), + measurement=int(1.37e9), ), n_rotation_layers=int(2.05e8), ), @@ -65,7 +70,10 @@ class Test: alg=AlgorithmSummary( n_algo_qubits=12581, n_logical_gates=GateCounts( - t=12, rotation=12, toffoli=int(3.73e9), measurement=int(1.08e9) + t=12, + rotation_epsilons={1 / 3 / 12: 12}, + toffoli=int(3.73e9), + measurement=int(1.08e9), ), n_rotation_layers=12, ), diff --git a/qualtran/surface_code/ui.py b/qualtran/surface_code/ui.py index 85f4010c4..5cf74307f 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,22 @@ 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 = 1e-3 / rotations # TODO this should not be a magic number + algorithm = AlgorithmSummary( n_algo_qubits=qubits, n_logical_gates=GateCounts( - measurement=measurements, t=ts, toffoli=toffolis, rotation=rotations + measurement=measurements, + t=ts, + toffoli=toffolis, + rotation_epsilons={rotation_eps: 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', ) From 0b7a9e3c354f865a5b50a408c7621b22b8a756fd Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Wed, 7 Aug 2024 12:54:59 -0700 Subject: [PATCH 02/19] bin epsilons --- qualtran/resource_counting/_bloq_counts.py | 76 +++++++++++++++---- .../resource_counting/_bloq_counts_test.py | 4 +- .../surface_code/algorithm_summary_test.py | 4 +- .../surface_code/beverland_et_al_model.py | 11 +-- .../beverland_et_al_model_test.py | 21 +++-- qualtran/surface_code/ui.py | 8 +- 6 files changed, 84 insertions(+), 40 deletions(-) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index 6c48ea630..b716b9e30 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -13,7 +13,7 @@ # limitations under the License. import logging from collections import Counter, defaultdict -from typing import Callable, Dict, Mapping, Sequence, Tuple, TYPE_CHECKING +from typing import Callable, Dict, Iterator, Mapping, Sequence, Tuple, TYPE_CHECKING import attrs import networkx as nx @@ -112,7 +112,7 @@ def __str__(self): return f'{self.gateset_name} counts' -def _mapping_to_counter(mapping: Mapping[float, int]) -> Counter[float]: +def _mapping_to_counter(mapping: Mapping[int, int]) -> Counter[int]: if isinstance(mapping, Counter): return mapping return Counter(mapping) @@ -132,29 +132,74 @@ class GateCounts: and_bloq: int = 0 clifford: int = 0 measurement: int = 0 - rotation_epsilons: Counter[float] = field(factory=Counter, converter=_mapping_to_counter) + binned_rotation_epsilons: Counter[int] = field(factory=Counter, converter=_mapping_to_counter) + eps_bin_prec: int = 10 + + @classmethod + def from_rotation_with_eps(cls, eps: float, *, eps_bin_prec: int = 10, n_rotations: int = 1): + """Construct a GateCount with a rotation of precision `eps`. + + Args: + eps: precision to synthesize the rotation(s). + eps_bin_prec: number of bits to approximate `eps` to, defaults to 10. + n_rotations: number of rotations, defaults to 1. + """ + eps_bin = int(eps * 2**eps_bin_prec) + return cls(binned_rotation_epsilons=Counter({eps_bin: n_rotations})) + + def with_rotation_eps_bin_prec(self, new_eps_bin_prec: int) -> 'GateCounts': + """Returns `GateCounts` with a new bin precision for rotation epsilons.""" + if new_eps_bin_prec == self.eps_bin_prec: + return self + + def _get_new_eps_bin(eps_bin): + return int(eps_bin * 2 ** (new_eps_bin_prec - self.eps_bin_prec)) + + new_binned_rotation_epsilons = Counter( + { + _get_new_eps_bin(eps_bin): n_rot + for eps_bin, n_rot in self.binned_rotation_epsilons.items() + } + ) + + return attrs.evolve( + self, + binned_rotation_epsilons=new_binned_rotation_epsilons, + eps_bin_prec=new_eps_bin_prec, + ) @property def rotation(self): + # TODO return correct value and document precisely. from qualtran.cirq_interop.t_complexity_protocol import TComplexity return sum( - n_rotations * int(TComplexity.rotation_cost(eps)) - for eps, n_rotations in self.rotation_epsilons.items() + n_rotations * int(TComplexity.rotation_cost(eps_bin / 2**self.eps_bin_prec)) + for eps_bin, n_rotations in self.binned_rotation_epsilons.items() ) + def iter_rotations_with_epsilon(self) -> Iterator[tuple[float, int]]: + """Iterate through the rotation precisions (epsilon) and their frequency.""" + for eps_bin, n_rot in self.binned_rotation_epsilons.items(): + yield eps_bin / 2**self.eps_bin_prec, n_rot + def __add__(self, other): if not isinstance(other, GateCounts): raise TypeError(f"Can only add other `GateCounts` objects, not {self}") + eps_bin_prec = max(self.eps_bin_prec, other.eps_bin_prec) + this = self.with_rotation_eps_bin_prec(eps_bin_prec) + other = other.with_rotation_eps_bin_prec(other.eps_bin_prec) + return GateCounts( - t=self.t + other.t, - toffoli=self.toffoli + other.toffoli, - cswap=self.cswap + other.cswap, - and_bloq=self.and_bloq + other.and_bloq, - clifford=self.clifford + other.clifford, - measurement=self.measurement + other.measurement, - rotation_epsilons=self.rotation_epsilons + other.rotation_epsilons, + t=this.t + other.t, + toffoli=this.toffoli + other.toffoli, + cswap=this.cswap + other.cswap, + and_bloq=this.and_bloq + other.and_bloq, + clifford=this.clifford + other.clifford, + measurement=this.measurement + other.measurement, + binned_rotation_epsilons=this.binned_rotation_epsilons + other.binned_rotation_epsilons, + eps_bin_prec=eps_bin_prec, ) def __mul__(self, other): @@ -165,7 +210,10 @@ def __mul__(self, other): and_bloq=other * self.and_bloq, clifford=other * self.clifford, measurement=other * self.measurement, - rotation_epsilons=Counter({k: other * v for k, v in self.rotation_epsilons.items()}), + binned_rotation_epsilons=Counter( + {eps_bin: other * n_rot for eps_bin, n_rot in self.binned_rotation_epsilons.items()} + ), + eps_bin_prec=self.eps_bin_prec, ) def __rmul__(self, other): @@ -280,7 +328,7 @@ def compute(self, bloq: 'Bloq', get_callee_cost: Callable[['Bloq'], GateCounts]) if bloq_is_rotation(bloq): assert isinstance(bloq, _HasEps) - return GateCounts(rotation_epsilons={bloq.eps: 1}) + 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 ae5a4c2d2..85598587b 100644 --- a/qualtran/resource_counting/_bloq_counts_test.py +++ b/qualtran/resource_counting/_bloq_counts_test.py @@ -76,13 +76,13 @@ def test_qec_gates_cost(): # Rotations [ basic_gates.ZPowGate(exponent=0.1, global_shift=0.0, eps=1e-11), - GateCounts(rotation_epsilons={1e-11: 1}), + GateCounts.from_rotation_with_eps(1e-11), ], [ rotations.phase_gradient.PhaseGradientUnitary( bitsize=10, exponent=1, is_controlled=False, eps=1e-10 ), - GateCounts(rotation_epsilons={1e-10: 10}), + GateCounts.from_rotation_with_eps(1e-10), ], # Recursive [ diff --git a/qualtran/surface_code/algorithm_summary_test.py b/qualtran/surface_code/algorithm_summary_test.py index 59318f8a6..5478d7ce2 100644 --- a/qualtran/surface_code/algorithm_summary_test.py +++ b/qualtran/surface_code/algorithm_summary_test.py @@ -39,7 +39,7 @@ [ basic_gates.ZPowGate(exponent=0.1, global_shift=0.0, eps=1e-11), AlgorithmSummary( - n_algo_qubits=1, n_logical_gates=GateCounts(rotation_epsilons={1e-11: 1}) + n_algo_qubits=1, n_logical_gates=GateCounts.from_rotation_with_eps(1e-11) ), ], [ @@ -47,7 +47,7 @@ bitsize=10, exponent=1, is_controlled=False, eps=1e-10 ), AlgorithmSummary( - n_algo_qubits=10, n_logical_gates=GateCounts(rotation_epsilons={1e-10: 10}) + n_algo_qubits=10, n_logical_gates=GateCounts.from_rotation_with_eps(1e-10) ), ], [ diff --git a/qualtran/surface_code/beverland_et_al_model.py b/qualtran/surface_code/beverland_et_al_model.py index b75287f09..2100b57a4 100644 --- a/qualtran/surface_code/beverland_et_al_model.py +++ b/qualtran/surface_code/beverland_et_al_model.py @@ -116,11 +116,12 @@ def n_discrete_logical_gates( rotation_model: Cost model used to compute the number of T gates needed to approximate rotations. """ - rotation_epsilons: dict[float, int] = alg.n_logical_gates.rotation_epsilons - ret = attrs.evolve(alg.n_logical_gates, rotation_epsilons={}) - if rotation_epsilons: - rotation_model.preparation_overhead(min(eps for eps in rotation_epsilons.values())) - for eps, n_rotations in rotation_epsilons.items(): + n_logical_gates = alg.n_logical_gates + ret = attrs.evolve(alg.n_logical_gates, binned_rotation_epsilons={}) + if n_logical_gates.binned_rotation_epsilons: + min_eps_rot = min(eps for eps, _ in n_logical_gates.iter_rotations_with_epsilon()) + rotation_model.preparation_overhead(min(min_eps_rot, eps_syn)) # TODO is this correct? + for eps, n_rotations in n_logical_gates.iter_rotations_with_epsilon(): ret += n_rotations * rotation_model.rotation_cost(eps) return ret diff --git a/qualtran/surface_code/beverland_et_al_model_test.py b/qualtran/surface_code/beverland_et_al_model_test.py index e054bc579..849e24aca 100644 --- a/qualtran/surface_code/beverland_et_al_model_test.py +++ b/qualtran/surface_code/beverland_et_al_model_test.py @@ -38,8 +38,9 @@ class Test: Test( alg=AlgorithmSummary( n_algo_qubits=100, - n_logical_gates=GateCounts( - rotation_epsilons={1e-3 / 30_000: 30_000}, measurement=int(1.4e6) + n_logical_gates=( + GateCounts.from_rotation_with_eps(1e-3 / 30_000, n_rotations=30_000) + + GateCounts(measurement=int(1.4e6)) ), n_rotation_layers=501, ), @@ -52,11 +53,9 @@ class Test: Test( alg=AlgorithmSummary( n_algo_qubits=1318, - n_logical_gates=GateCounts( - t=int(5.53e7), - rotation_epsilons={1e-2 / 2.06e8: 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(1e-2 / 2.06e8, n_rotations=int(2.06e8)) ), n_rotation_layers=int(2.05e8), ), @@ -69,11 +68,9 @@ class Test: Test( alg=AlgorithmSummary( n_algo_qubits=12581, - n_logical_gates=GateCounts( - t=12, - rotation_epsilons={1 / 3 / 12: 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(1 / 3 / 12, n_rotations=12) ), n_rotation_layers=12, ), diff --git a/qualtran/surface_code/ui.py b/qualtran/surface_code/ui.py index 5cf74307f..33efc66f2 100644 --- a/qualtran/surface_code/ui.py +++ b/qualtran/surface_code/ui.py @@ -535,11 +535,9 @@ def update( algorithm = AlgorithmSummary( n_algo_qubits=qubits, - n_logical_gates=GateCounts( - measurement=measurements, - t=ts, - toffoli=toffolis, - rotation_epsilons={rotation_eps: 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, ) From b6c207d9fc6a721ca474cb79d3c2ce0a1a8717f8 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Wed, 7 Aug 2024 13:03:12 -0700 Subject: [PATCH 03/19] avoid 0s --- qualtran/resource_counting/_bloq_counts.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index b716b9e30..e9d560239 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -17,6 +17,7 @@ import attrs import networkx as nx +import numpy as np from attrs import field, frozen from ._call_graph import get_bloq_callee_counts @@ -133,10 +134,10 @@ class GateCounts: clifford: int = 0 measurement: int = 0 binned_rotation_epsilons: Counter[int] = field(factory=Counter, converter=_mapping_to_counter) - eps_bin_prec: int = 10 + eps_bin_prec: int = 20 @classmethod - def from_rotation_with_eps(cls, eps: float, *, eps_bin_prec: int = 10, n_rotations: int = 1): + def from_rotation_with_eps(cls, eps: float, *, eps_bin_prec: int = 20, n_rotations: int = 1): """Construct a GateCount with a rotation of precision `eps`. Args: @@ -144,7 +145,9 @@ def from_rotation_with_eps(cls, eps: float, *, eps_bin_prec: int = 10, n_rotatio eps_bin_prec: number of bits to approximate `eps` to, defaults to 10. n_rotations: number of rotations, defaults to 1. """ - eps_bin = int(eps * 2**eps_bin_prec) + eps_bin = int(np.ceil(eps * 2**eps_bin_prec)) + # treat any eps < 2**(-eps_bin_prec) as 2**(-eps_bin_prec) + eps_bin = max(1, eps_bin) return cls(binned_rotation_epsilons=Counter({eps_bin: n_rotations})) def with_rotation_eps_bin_prec(self, new_eps_bin_prec: int) -> 'GateCounts': From 931f1bc7cf04908277a8a29df6b9ffa7a7138847 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Wed, 7 Aug 2024 13:07:24 -0700 Subject: [PATCH 04/19] fix tests --- qualtran/resource_counting/_bloq_counts.py | 9 +++++++-- qualtran/resource_counting/_bloq_counts_test.py | 2 +- qualtran/surface_code/algorithm_summary_test.py | 3 ++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index e9d560239..153d204df 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -133,7 +133,9 @@ class GateCounts: and_bloq: int = 0 clifford: int = 0 measurement: int = 0 - binned_rotation_epsilons: Counter[int] = field(factory=Counter, converter=_mapping_to_counter) + binned_rotation_epsilons: Counter[int] = field( + factory=Counter, converter=_mapping_to_counter, eq=lambda d: tuple(d.items()) + ) eps_bin_prec: int = 20 @classmethod @@ -235,8 +237,11 @@ def asdict(self): d = attrs.asdict(self) def _keep(key, value) -> bool: - if key == 'rotation_epsilons': + if key == 'binned_rotation_epsilons': return value + if key == 'eps_bin_prec': + # rotations non-empty + return len(self.binned_rotation_epsilons) > 0 return value > 0 return {k: v for k, v in d.items() if _keep(k, v)} diff --git a/qualtran/resource_counting/_bloq_counts_test.py b/qualtran/resource_counting/_bloq_counts_test.py index 85598587b..83b134a95 100644 --- a/qualtran/resource_counting/_bloq_counts_test.py +++ b/qualtran/resource_counting/_bloq_counts_test.py @@ -82,7 +82,7 @@ def test_qec_gates_cost(): rotations.phase_gradient.PhaseGradientUnitary( bitsize=10, exponent=1, is_controlled=False, eps=1e-10 ), - GateCounts.from_rotation_with_eps(1e-10), + GateCounts.from_rotation_with_eps(1e-10, n_rotations=10), ], # Recursive [ diff --git a/qualtran/surface_code/algorithm_summary_test.py b/qualtran/surface_code/algorithm_summary_test.py index 5478d7ce2..0c2540b34 100644 --- a/qualtran/surface_code/algorithm_summary_test.py +++ b/qualtran/surface_code/algorithm_summary_test.py @@ -47,7 +47,8 @@ bitsize=10, exponent=1, is_controlled=False, eps=1e-10 ), AlgorithmSummary( - n_algo_qubits=10, n_logical_gates=GateCounts.from_rotation_with_eps(1e-10) + n_algo_qubits=10, + n_logical_gates=GateCounts.from_rotation_with_eps(1e-10, n_rotations=10), ), ], [ From 5272c9f4060254f36dd73925a4fb927d9eb41360 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Wed, 7 Aug 2024 15:33:18 -0700 Subject: [PATCH 05/19] use only n_rotations for Beverland et.al. model --- qualtran/resource_counting/_bloq_counts.py | 43 ++++++++++--------- .../surface_code/beverland_et_al_model.py | 13 +++--- .../beverland_et_al_model_test.py | 8 ++-- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index 153d204df..30023e902 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -173,16 +173,6 @@ def _get_new_eps_bin(eps_bin): eps_bin_prec=new_eps_bin_prec, ) - @property - def rotation(self): - # TODO return correct value and document precisely. - from qualtran.cirq_interop.t_complexity_protocol import TComplexity - - return sum( - n_rotations * int(TComplexity.rotation_cost(eps_bin / 2**self.eps_bin_prec)) - for eps_bin, n_rotations in self.binned_rotation_epsilons.items() - ) - def iter_rotations_with_epsilon(self) -> Iterator[tuple[float, int]]: """Iterate through the rotation precisions (epsilon) and their frequency.""" for eps_bin, n_rot in self.binned_rotation_epsilons.items(): @@ -246,12 +236,18 @@ def _keep(key, value) -> bool: return {k: v for k, v in d.items() if _keep(k, v)} + @property + def rotation_to_t(self) -> int: + """Total number of T Gates for the rotations.""" + from qualtran.cirq_interop.t_complexity_protocol import TComplexity + + return sum( + n_rotations * int(TComplexity.rotation_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. @@ -266,14 +262,18 @@ def total_t_count( + ts_per_toffoli * self.toffoli + ts_per_cswap * self.cswap + ts_per_and_bloq * self.and_bloq - + ts_per_rotation * self.rotation + + self.rotation_to_t ) - def total_t_and_ccz_count(self, ts_per_rotation: int = 11) -> Dict[str, int]: + def total_t_and_ccz_count(self) -> Dict[str, int]: n_ccz = self.toffoli + self.cswap + self.and_bloq - n_t = self.t + ts_per_rotation * self.rotation + n_t = self.t + self.rotation_to_t return {'n_t': n_t, 'n_ccz': n_ccz} + def n_rotation_ignoring_eps(self) -> int: + """Total number of rotations, ignoring the individual precisions.""" + return sum(self.binned_rotation_epsilons.values()) + def total_beverland_count(self) -> Dict[str, int]: r"""Counts used by Beverland. et. al. using notation from the reference. @@ -286,17 +286,20 @@ def total_beverland_count(self) -> Dict[str, int]: 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.n_rotation_ignoring_eps() return { 'meas': self.measurement, - 'R': self.rotation, + 'R': rotation, 'T': self.t, 'Tof': toffoli, - 'D_R': self.rotation, + 'D_R': rotation, } diff --git a/qualtran/surface_code/beverland_et_al_model.py b/qualtran/surface_code/beverland_et_al_model.py index 2100b57a4..a02b51d1d 100644 --- a/qualtran/surface_code/beverland_et_al_model.py +++ b/qualtran/surface_code/beverland_et_al_model.py @@ -116,13 +116,14 @@ def n_discrete_logical_gates( rotation_model: Cost model used to compute the number of T gates needed to approximate rotations. """ - n_logical_gates = alg.n_logical_gates + n_rotations: int = alg.n_logical_gates.n_rotation_ignoring_eps() ret = attrs.evolve(alg.n_logical_gates, binned_rotation_epsilons={}) - if n_logical_gates.binned_rotation_epsilons: - min_eps_rot = min(eps for eps, _ in n_logical_gates.iter_rotations_with_epsilon()) - rotation_model.preparation_overhead(min(min_eps_rot, eps_syn)) # TODO is this correct? - for eps, n_rotations in n_logical_gates.iter_rotations_with_epsilon(): - ret += n_rotations * rotation_model.rotation_cost(eps) + if n_rotations > 0: + ret = ( + ret + + rotation_model.preparation_overhead(eps_syn) + + n_rotations * rotation_model.rotation_cost(eps_syn / n_rotations) + ) return ret diff --git a/qualtran/surface_code/beverland_et_al_model_test.py b/qualtran/surface_code/beverland_et_al_model_test.py index 849e24aca..d2ecd4d26 100644 --- a/qualtran/surface_code/beverland_et_al_model_test.py +++ b/qualtran/surface_code/beverland_et_al_model_test.py @@ -39,13 +39,13 @@ class Test: alg=AlgorithmSummary( n_algo_qubits=100, n_logical_gates=( - GateCounts.from_rotation_with_eps(1e-3 / 30_000, n_rotations=30_000) + GateCounts.from_rotation_with_eps(0, n_rotations=30_000) + GateCounts(measurement=int(1.4e6)) ), n_rotation_layers=501, ), error_budget=1e-3, - c_min=2.5e6, + c_min=1.5e6, time_steps=1.5e5, code_distance=9, t_states=602000, @@ -55,7 +55,7 @@ class Test: n_algo_qubits=1318, n_logical_gates=( GateCounts(t=int(5.53e7), toffoli=int(1.35e11), measurement=int(1.37e9)) - + GateCounts.from_rotation_with_eps(1e-2 / 2.06e8, n_rotations=int(2.06e8)) + + GateCounts.from_rotation_with_eps(0, n_rotations=int(2.06e8)) ), n_rotation_layers=int(2.05e8), ), @@ -70,7 +70,7 @@ class Test: n_algo_qubits=12581, n_logical_gates=( GateCounts(t=12, toffoli=int(3.73e9), measurement=int(1.08e9)) - + GateCounts.from_rotation_with_eps(1 / 3 / 12, n_rotations=12) + + GateCounts.from_rotation_with_eps(0, n_rotations=12) ), n_rotation_layers=12, ), From 5f862d3587c1241cc789558240cb6e9c7af50b36 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Wed, 7 Aug 2024 15:39:50 -0700 Subject: [PATCH 06/19] fix notebook --- .../surface_code/beverland_et_al_model.ipynb | 18 +++++++++--------- .../surface_code/beverland_et_al_model_test.py | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) 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_test.py b/qualtran/surface_code/beverland_et_al_model_test.py index d2ecd4d26..c1d20a2f9 100644 --- a/qualtran/surface_code/beverland_et_al_model_test.py +++ b/qualtran/surface_code/beverland_et_al_model_test.py @@ -111,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') From a1a8620420381d396589873e2a5a640447c0806a Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Wed, 7 Aug 2024 15:53:31 -0700 Subject: [PATCH 07/19] use error_budget --- qualtran/surface_code/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualtran/surface_code/ui.py b/qualtran/surface_code/ui.py index 33efc66f2..4c1d7e29f 100644 --- a/qualtran/surface_code/ui.py +++ b/qualtran/surface_code/ui.py @@ -531,7 +531,7 @@ 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 = 1e-3 / rotations # TODO this should not be a magic number + rotation_eps = error_budget / rotations algorithm = AlgorithmSummary( n_algo_qubits=qubits, From f5f69aa81f853c9304a71991fa2ef694d15dd401 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Wed, 7 Aug 2024 16:25:09 -0700 Subject: [PATCH 08/19] move the T-complexity formula to `GateCounts` --- qualtran/cirq_interop/t_complexity_protocol.py | 6 ++++-- qualtran/resource_counting/_bloq_counts.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/qualtran/cirq_interop/t_complexity_protocol.py b/qualtran/cirq_interop/t_complexity_protocol.py index ce39fb5e6..174846910 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, Controlled, 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 30023e902..397c7ca5f 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -20,6 +20,8 @@ import numpy as np from attrs import field, frozen +from qualtran.symbolics import ceil, log2, SymbolicFloat + from ._call_graph import get_bloq_callee_counts from ._costing import CostKey from .classify_bloqs import bloq_is_clifford, bloq_is_rotation @@ -236,6 +238,16 @@ def _keep(key, value) -> bool: return {k: v for k, v in d.items() if _keep(k, v)} + @staticmethod + def rotation_t_cost(eps: SymbolicFloat) -> SymbolicFloat: + """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) + @property def rotation_to_t(self) -> int: """Total number of T Gates for the rotations.""" From 9b45a0b29c5817c65291eaa301a5421c9d655c06 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Wed, 7 Aug 2024 17:22:07 -0700 Subject: [PATCH 09/19] symbolic --- qualtran/resource_counting/_bloq_counts.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index f1b3e870c..317f57c6c 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -21,7 +21,7 @@ import sympy from attrs import field, frozen -from qualtran.symbolics import ceil, log2, SymbolicFloat, SymbolicInt +from qualtran.symbolics import ceil, log2, ssum, SymbolicFloat, SymbolicInt from ._call_graph import get_bloq_callee_counts from ._costing import CostKey @@ -176,7 +176,7 @@ def _get_new_eps_bin(eps_bin): eps_bin_prec=new_eps_bin_prec, ) - def iter_rotations_with_epsilon(self) -> Iterator[tuple[float, int]]: + def iter_rotations_with_epsilon(self) -> Iterator[tuple[float, SymbolicInt]]: """Iterate through the rotation precisions (epsilon) and their frequency.""" for eps_bin, n_rot in self.binned_rotation_epsilons.items(): yield eps_bin / 2**self.eps_bin_prec, n_rot @@ -243,7 +243,7 @@ def _keep(key, value) -> bool: return {k: v for k, v in d.items() if _keep(k, v)} @staticmethod - def rotation_t_cost(eps: SymbolicFloat) -> SymbolicFloat: + def rotation_t_cost(eps: SymbolicFloat) -> SymbolicInt: """T-cost of a single Z rotation with precision `eps`. References: @@ -253,12 +253,10 @@ def rotation_t_cost(eps: SymbolicFloat) -> SymbolicFloat: return ceil(1.149 * log2(1.0 / eps) + 9.2) @property - def rotation_to_t(self) -> int: + def rotation_to_t(self) -> SymbolicInt: """Total number of T Gates for the rotations.""" - from qualtran.cirq_interop.t_complexity_protocol import TComplexity - - return sum( - n_rotations * int(TComplexity.rotation_cost(eps)) + return ssum( + n_rotations * self.rotation_t_cost(eps) for eps, n_rotations in self.iter_rotations_with_epsilon() ) @@ -286,9 +284,9 @@ def total_t_and_ccz_count(self) -> Dict[str, SymbolicInt]: n_t = self.t + self.rotation_to_t return {'n_t': n_t, 'n_ccz': n_ccz} - def n_rotation_ignoring_eps(self) -> int: + def n_rotation_ignoring_eps(self) -> SymbolicInt: """Total number of rotations, ignoring the individual precisions.""" - return sum(self.binned_rotation_epsilons.values()) + 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. From 4f434e1659616fe5690d00ad8edf33e250dfbdef Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Wed, 7 Aug 2024 17:28:15 -0700 Subject: [PATCH 10/19] cleanup docstring --- qualtran/resource_counting/_bloq_counts.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index 317f57c6c..a2049b54c 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -267,9 +267,6 @@ def total_t_count( 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 From 053fda09c9a79cfdc322ecd53a61407216685037 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Thu, 8 Aug 2024 11:57:50 -0700 Subject: [PATCH 11/19] use `np.format_float_scientific` --- qualtran/resource_counting/_bloq_counts.py | 69 ++++++------------- .../resource_counting/_bloq_counts_test.py | 2 +- .../surface_code/algorithm_summary_test.py | 2 +- .../surface_code/beverland_et_al_model.py | 2 +- 4 files changed, 24 insertions(+), 51 deletions(-) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index a2049b54c..0e4db88d8 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -116,7 +116,11 @@ def __str__(self): return f'{self.gateset_name} counts' -def _mapping_to_counter(mapping: Mapping[int, int]) -> Counter[int]: +FloatRepr_T = str +"""The type to represent floats as, to use as safe keys in mappings.""" + + +def _mapping_to_counter(mapping: Mapping[FloatRepr_T, int]) -> Counter[FloatRepr_T]: if isinstance(mapping, Counter): return mapping return Counter(mapping) @@ -136,68 +140,40 @@ class GateCounts: and_bloq: SymbolicInt = 0 clifford: SymbolicInt = 0 measurement: SymbolicInt = 0 - binned_rotation_epsilons: Counter[int] = field( + binned_rotation_epsilons: Counter[FloatRepr_T] = field( factory=Counter, converter=_mapping_to_counter, eq=lambda d: tuple(d.items()) ) - eps_bin_prec: int = 20 @classmethod - def from_rotation_with_eps(cls, eps: float, *, eps_bin_prec: int = 20, n_rotations: int = 1): + def from_rotation_with_eps(cls, eps: float, *, n_rotations: int = 1, eps_repr_prec: int = 10): """Construct a GateCount with a rotation of precision `eps`. Args: eps: precision to synthesize the rotation(s). - eps_bin_prec: number of bits to approximate `eps` to, defaults to 10. + eps_repr_prec: number of digits to approximate `eps` to. Uses 10 by default. + See `np.format_float_scientific` for more details. n_rotations: number of rotations, defaults to 1. """ - eps_bin = int(np.ceil(eps * 2**eps_bin_prec)) - # treat any eps < 2**(-eps_bin_prec) as 2**(-eps_bin_prec) - eps_bin = max(1, eps_bin) + eps_bin = np.format_float_scientific(eps, precision=eps_repr_prec, unique=False) return cls(binned_rotation_epsilons=Counter({eps_bin: n_rotations})) - def with_rotation_eps_bin_prec(self, new_eps_bin_prec: int) -> 'GateCounts': - """Returns `GateCounts` with a new bin precision for rotation epsilons.""" - if new_eps_bin_prec == self.eps_bin_prec: - return self - - def _get_new_eps_bin(eps_bin): - return int(eps_bin * 2 ** (new_eps_bin_prec - self.eps_bin_prec)) - - new_binned_rotation_epsilons = Counter( - { - _get_new_eps_bin(eps_bin): n_rot - for eps_bin, n_rot in self.binned_rotation_epsilons.items() - } - ) - - return attrs.evolve( - self, - binned_rotation_epsilons=new_binned_rotation_epsilons, - eps_bin_prec=new_eps_bin_prec, - ) - def iter_rotations_with_epsilon(self) -> Iterator[tuple[float, SymbolicInt]]: """Iterate through the rotation precisions (epsilon) and their frequency.""" for eps_bin, n_rot in self.binned_rotation_epsilons.items(): - yield eps_bin / 2**self.eps_bin_prec, n_rot + yield float(eps_bin), n_rot def __add__(self, other): if not isinstance(other, GateCounts): raise TypeError(f"Can only add other `GateCounts` objects, not {self}") - eps_bin_prec = max(self.eps_bin_prec, other.eps_bin_prec) - this = self.with_rotation_eps_bin_prec(eps_bin_prec) - other = other.with_rotation_eps_bin_prec(other.eps_bin_prec) - return GateCounts( - t=this.t + other.t, - toffoli=this.toffoli + other.toffoli, - cswap=this.cswap + other.cswap, - and_bloq=this.and_bloq + other.and_bloq, - clifford=this.clifford + other.clifford, - measurement=this.measurement + other.measurement, - binned_rotation_epsilons=this.binned_rotation_epsilons + other.binned_rotation_epsilons, - eps_bin_prec=eps_bin_prec, + t=self.t + other.t, + toffoli=self.toffoli + other.toffoli, + cswap=self.cswap + other.cswap, + and_bloq=self.and_bloq + other.and_bloq, + clifford=self.clifford + other.clifford, + measurement=self.measurement + other.measurement, + binned_rotation_epsilons=self.binned_rotation_epsilons + other.binned_rotation_epsilons, ) def __mul__(self, other): @@ -211,7 +187,6 @@ def __mul__(self, other): binned_rotation_epsilons=Counter( {eps_bin: other * n_rot for eps_bin, n_rot in self.binned_rotation_epsilons.items()} ), - eps_bin_prec=self.eps_bin_prec, ) def __rmul__(self, other): @@ -235,9 +210,6 @@ def _is_nonzero(v): def _keep(key, value) -> bool: if key == 'binned_rotation_epsilons': return value - if key == 'eps_bin_prec': - # rotations non-empty - return len(self.binned_rotation_epsilons) > 0 return _is_nonzero(value) return {k: v for k, v in d.items() if _keep(k, v)} @@ -281,7 +253,8 @@ def total_t_and_ccz_count(self) -> Dict[str, SymbolicInt]: n_t = self.t + self.rotation_to_t return {'n_t': n_t, 'n_ccz': n_ccz} - def n_rotation_ignoring_eps(self) -> SymbolicInt: + @property + def rotations_ignoring_eps(self) -> SymbolicInt: """Total number of rotations, ignoring the individual precisions.""" return ssum(self.binned_rotation_epsilons.values()) @@ -304,7 +277,7 @@ def total_beverland_count(self) -> Dict[str, SymbolicInt]: Equation D3. """ toffoli = self.toffoli + self.and_bloq + self.cswap - rotation = self.n_rotation_ignoring_eps() + rotation = self.rotations_ignoring_eps return { 'meas': self.measurement, 'R': rotation, diff --git a/qualtran/resource_counting/_bloq_counts_test.py b/qualtran/resource_counting/_bloq_counts_test.py index 9a92137a3..4e11235b6 100644 --- a/qualtran/resource_counting/_bloq_counts_test.py +++ b/qualtran/resource_counting/_bloq_counts_test.py @@ -86,7 +86,7 @@ def test_qec_gates_cost(): rotations.phase_gradient.PhaseGradientUnitary( bitsize=10, exponent=1, is_controlled=False, eps=1e-10 ), - GateCounts.from_rotation_with_eps(1e-10, n_rotations=10), + GateCounts.from_rotation_with_eps(1e-11, n_rotations=10), ], # Recursive [ diff --git a/qualtran/surface_code/algorithm_summary_test.py b/qualtran/surface_code/algorithm_summary_test.py index 0c2540b34..6b975ac59 100644 --- a/qualtran/surface_code/algorithm_summary_test.py +++ b/qualtran/surface_code/algorithm_summary_test.py @@ -48,7 +48,7 @@ ), AlgorithmSummary( n_algo_qubits=10, - n_logical_gates=GateCounts.from_rotation_with_eps(1e-10, n_rotations=10), + n_logical_gates=GateCounts.from_rotation_with_eps(1e-11, n_rotations=10), ), ], [ diff --git a/qualtran/surface_code/beverland_et_al_model.py b/qualtran/surface_code/beverland_et_al_model.py index ea3d2e9c4..454bcd8fb 100644 --- a/qualtran/surface_code/beverland_et_al_model.py +++ b/qualtran/surface_code/beverland_et_al_model.py @@ -117,7 +117,7 @@ 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.n_rotation_ignoring_eps() + 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 = ( From 886871bd2a6bf5605173f2b962a8935be5a509d7 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Thu, 8 Aug 2024 12:14:41 -0700 Subject: [PATCH 12/19] `total_rotations_as_t` --- qualtran/resource_counting/_bloq_counts.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index 0e4db88d8..cfa896e46 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -224,8 +224,7 @@ def rotation_t_cost(eps: SymbolicFloat) -> SymbolicInt: """ return ceil(1.149 * log2(1.0 / eps) + 9.2) - @property - def rotation_to_t(self) -> SymbolicInt: + def total_rotations_as_t(self) -> SymbolicInt: """Total number of T Gates for the rotations.""" return ssum( n_rotations * self.rotation_t_cost(eps) @@ -245,12 +244,12 @@ def total_t_count( + ts_per_toffoli * self.toffoli + ts_per_cswap * self.cswap + ts_per_and_bloq * self.and_bloq - + self.rotation_to_t + + self.total_rotations_as_t() ) def total_t_and_ccz_count(self) -> Dict[str, SymbolicInt]: n_ccz = self.toffoli + self.cswap + self.and_bloq - n_t = self.t + self.rotation_to_t + n_t = self.t + self.total_rotations_as_t() return {'n_t': n_t, 'n_ccz': n_ccz} @property From 4cd3a36535cfb71d1438ba199b5261dd48c456bd Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Thu, 8 Aug 2024 12:23:32 -0700 Subject: [PATCH 13/19] support symbolic `eps` --- qualtran/resource_counting/_bloq_counts.py | 24 +++++++++++++------ .../resource_counting/_bloq_counts_test.py | 19 +++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index cfa896e46..10af398f4 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -13,7 +13,7 @@ # limitations under the License. import logging from collections import Counter, defaultdict -from typing import Callable, Dict, Iterator, Mapping, Sequence, Tuple, TYPE_CHECKING +from typing import Callable, Dict, Iterator, Mapping, Sequence, Tuple, TYPE_CHECKING, Union import attrs import networkx as nx @@ -21,7 +21,7 @@ import sympy from attrs import field, frozen -from qualtran.symbolics import ceil, log2, ssum, SymbolicFloat, SymbolicInt +from qualtran.symbolics import ceil, is_symbolic, log2, ssum, SymbolicFloat, SymbolicInt from ._call_graph import get_bloq_callee_counts from ._costing import CostKey @@ -116,7 +116,7 @@ def __str__(self): return f'{self.gateset_name} counts' -FloatRepr_T = str +FloatRepr_T = Union[str, sympy.Expr] """The type to represent floats as, to use as safe keys in mappings.""" @@ -145,22 +145,32 @@ class GateCounts: ) @classmethod - def from_rotation_with_eps(cls, eps: float, *, n_rotations: int = 1, eps_repr_prec: int = 10): + 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. """ - eps_bin = np.format_float_scientific(eps, precision=eps_repr_prec, unique=False) + if is_symbolic(eps): + eps_bin: FloatRepr_T = eps + else: + eps_bin = np.format_float_scientific(eps, precision=eps_repr_prec, unique=False) return cls(binned_rotation_epsilons=Counter({eps_bin: n_rotations})) - def iter_rotations_with_epsilon(self) -> Iterator[tuple[float, SymbolicInt]]: + 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(): - yield float(eps_bin), n_rot + 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): diff --git a/qualtran/resource_counting/_bloq_counts_test.py b/qualtran/resource_counting/_bloq_counts_test.py index 4e11235b6..be61a2ec3 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 import Hadamard, TGate, Toffoli 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()) From 96ee81005c5d9175b822dae2ef27ac25008750df Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 12 Aug 2024 17:04:33 -0700 Subject: [PATCH 14/19] fix bug --- qualtran/bloqs/chemistry/sparse/sparse_test.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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'] From 3235c06cd1d17946577cfcde5967b91ca74b8bdc Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 12 Aug 2024 17:05:37 -0700 Subject: [PATCH 15/19] FloatReprT --- qualtran/resource_counting/_bloq_counts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index 10af398f4..db361531f 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -116,11 +116,11 @@ def __str__(self): return f'{self.gateset_name} counts' -FloatRepr_T = Union[str, sympy.Expr] +FloatReprT = Union[str, sympy.Expr] """The type to represent floats as, to use as safe keys in mappings.""" -def _mapping_to_counter(mapping: Mapping[FloatRepr_T, int]) -> Counter[FloatRepr_T]: +def _mapping_to_counter(mapping: Mapping[FloatReprT, int]) -> Counter[FloatReprT]: if isinstance(mapping, Counter): return mapping return Counter(mapping) @@ -140,7 +140,7 @@ class GateCounts: and_bloq: SymbolicInt = 0 clifford: SymbolicInt = 0 measurement: SymbolicInt = 0 - binned_rotation_epsilons: Counter[FloatRepr_T] = field( + binned_rotation_epsilons: Counter[FloatReprT] = field( factory=Counter, converter=_mapping_to_counter, eq=lambda d: tuple(d.items()) ) @@ -161,7 +161,7 @@ def from_rotation_with_eps( n_rotations: number of rotations, defaults to 1. """ if is_symbolic(eps): - eps_bin: FloatRepr_T = eps + eps_bin: FloatReprT = eps else: eps_bin = np.format_float_scientific(eps, precision=eps_repr_prec, unique=False) return cls(binned_rotation_epsilons=Counter({eps_bin: n_rotations})) From b1c2db6761f4cb1c137d83f2e2d1e79143e3285e Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 12 Aug 2024 17:11:02 -0700 Subject: [PATCH 16/19] docstring --- qualtran/resource_counting/_bloq_counts.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index db361531f..35c7667db 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -132,6 +132,20 @@ class GateCounts: 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 @@ -187,6 +201,7 @@ def __add__(self, other): ) def __mul__(self, other): + """Multiplies the frequency of each operation with `other`.""" return GateCounts( t=other * self.t, toffoli=other * self.toffoli, From a6da238811e656bdc09ccffe5b8248692042583a Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 12 Aug 2024 17:29:23 -0700 Subject: [PATCH 17/19] use min digits --- qualtran/resource_counting/_bloq_counts.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index 35c7667db..3928cf5ae 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -177,7 +177,9 @@ def from_rotation_with_eps( if is_symbolic(eps): eps_bin: FloatReprT = eps else: - eps_bin = np.format_float_scientific(eps, precision=eps_repr_prec, unique=False) + 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]]: From 265ce7011617b5cfbe139e4914a3488898e7221d Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Thu, 22 Aug 2024 23:08:11 -0700 Subject: [PATCH 18/19] no _HasEps --- qualtran/resource_counting/_bloq_counts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index 9b37cb180..46b6e3076 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -366,7 +366,7 @@ def compute(self, bloq: 'Bloq', get_callee_cost: Callable[['Bloq'], GateCounts]) return GateCounts() if bloq_is_rotation(bloq): - assert isinstance(bloq, _HasEps) + assert hasattr(bloq, 'eps') return GateCounts.from_rotation_with_eps(bloq.eps) # Recursive case From b03a755290d420a25db073ad9a6c0ca352404642 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Thu, 22 Aug 2024 23:19:45 -0700 Subject: [PATCH 19/19] hadamard t=2, cliff=9 --- qualtran/bloqs/basic_gates/hadamard.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) 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