-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use rotation eps
in GateCounts
#1255
base: main
Are you sure you want to change the base?
Changes from 12 commits
8feb870
0b7a9e3
b6c207d
931f1bc
5272c9f
5f862d3
a1a8620
3f36901
f5f69aa
35a1b4a
9b45a0b
4f434e1
053fda0
886871b
4cd3a36
5a0f65e
96ee810
3235c06
b1c2db6
a6da238
188ddff
ce6ee6b
1f0e0b7
265ce70
b03a755
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
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, log2, ssum, SymbolicFloat, SymbolicInt | ||
|
||
from ._call_graph import get_bloq_callee_counts | ||
from ._costing import CostKey | ||
|
@@ -115,6 +116,12 @@ def __str__(self): | |
return f'{self.gateset_name} counts' | ||
|
||
|
||
def _mapping_to_counter(mapping: Mapping[int, int]) -> Counter[int]: | ||
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. | ||
|
@@ -128,21 +135,69 @@ class GateCounts: | |
cswap: SymbolicInt = 0 | ||
and_bloq: SymbolicInt = 0 | ||
clifford: SymbolicInt = 0 | ||
rotation: SymbolicInt = 0 | ||
measurement: SymbolicInt = 0 | ||
binned_rotation_epsilons: Counter[int] = 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): | ||
"""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. | ||
anurudhp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
return cls(binned_rotation_epsilons=Counter({eps_bin: n_rotations})) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this still desperately needs a description of the data contained within the |
||
|
||
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 | ||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if you're adding one carefully-crafted rotation-containing GateCounts with a low eps_precision to a GateCounts that doesn't contain any rotations (it's just e.g. Ts and Toffolis) so it has the default eps_precision of 20 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Used |
||
this = self.with_rotation_eps_bin_prec(eps_bin_prec) | ||
other = other.with_rotation_eps_bin_prec(other.eps_bin_prec) | ||
anurudhp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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, | ||
rotation=self.rotation + other.rotation, | ||
measurement=self.measurement + other.measurement, | ||
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): | ||
|
@@ -152,8 +207,11 @@ 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, | ||
binned_rotation_epsilons=Counter( | ||
{eps_bin: other * n_rot for eps_bin, n_rot in self.binned_rotation_epsilons.items()} | ||
), | ||
Comment on lines
+219
to
+221
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so how does it work when adding or multiplying when there are values that differ only in precision There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the For add, it just combines the two counters together, and equal keys will get merged. (handled by |
||
eps_bin_prec=self.eps_bin_prec, | ||
) | ||
|
||
def __rmul__(self, other): | ||
|
@@ -174,36 +232,59 @@ 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 | ||
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)} | ||
|
||
@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) | ||
|
||
@property | ||
def rotation_to_t(self) -> SymbolicInt: | ||
anurudhp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""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.rotation_to_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.rotation_to_t | ||
return {'n_t': n_t, 'n_ccz': n_ccz} | ||
|
||
def n_rotation_ignoring_eps(self) -> SymbolicInt: | ||
"""Total number of rotations, ignoring the individual precisions.""" | ||
return ssum(self.binned_rotation_epsilons.values()) | ||
anurudhp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def total_beverland_count(self) -> Dict[str, SymbolicInt]: | ||
r"""Counts used by Beverland. et. al. using notation from the reference. | ||
|
||
|
@@ -216,17 +297,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.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, | ||
} | ||
|
||
|
||
|
@@ -239,6 +323,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 | ||
|
@@ -264,7 +349,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.from_rotation_with_eps(bloq.eps) | ||
|
||
# Recursive case | ||
totals = GateCounts() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please update the docstring with a thorough description of the representation of rotations.
Is it possible for someone to construct this class with inconsistent binned values vs the
eps_bin_prec
? What if someone puts in the epsilons as floats instead of 2**20*floatsdocument that the
eps_bin_prec
behavior during addition.eps_bin_prec
->epsilon_precision
? and then document that precision is measured by number of binary digits.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Used
np.format_scientific_float
to avoid such issues.