From f2eabb248e85c32d031ea639a218c757df01b19e Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 27 Aug 2024 00:14:24 +0000 Subject: [PATCH] First preparations for making And an atomic/leaf bloq (#1347) * First preparations for making And an atomic/leaf bloq * fix notebook due to change away from defaultdict * format --- .../controlled_add_or_subtract_test.py | 8 +-- qualtran/bloqs/arithmetic/permutation_test.py | 34 ++++++----- .../chemistry/df/double_factorization_test.py | 11 ++-- qualtran/bloqs/chemistry/df/prepare_test.py | 17 +++--- .../pbc/first_quantization/prepare_nu_test.py | 8 +-- .../pbc/first_quantization/prepare_uv_test.py | 8 +-- .../projectile/prepare_nu_test.py | 8 +-- .../projectile/prepare_uv_test.py | 8 +-- .../projectile/select_and_prepare_test.py | 4 +- .../chemistry/sparse/select_bloq_test.py | 5 +- qualtran/bloqs/chemistry/thc/thc.ipynb | 7 ++- qualtran/bloqs/data_loading/qrom_test.py | 46 ++++++-------- qualtran/bloqs/mcmt/and_bloq.ipynb | 32 +++++++++- qualtran/bloqs/mcmt/and_bloq.py | 61 +++++++++++++++++++ qualtran/resource_counting/_costing.py | 7 ++- qualtran/resource_counting/classify_bloqs.py | 7 ++- .../resource_counting/classify_bloqs_test.py | 11 ++-- qualtran/testing.py | 4 +- 18 files changed, 185 insertions(+), 101 deletions(-) diff --git a/qualtran/bloqs/arithmetic/controlled_add_or_subtract_test.py b/qualtran/bloqs/arithmetic/controlled_add_or_subtract_test.py index 89c71b28a..ff0236abf 100644 --- a/qualtran/bloqs/arithmetic/controlled_add_or_subtract_test.py +++ b/qualtran/bloqs/arithmetic/controlled_add_or_subtract_test.py @@ -36,9 +36,7 @@ _ctrl_add_or_sub_unsigned, ControlledAddOrSubtract, ) -from qualtran.bloqs.basic_gates import TGate -from qualtran.resource_counting import get_cost_value -from qualtran.resource_counting._bloq_counts import BloqCount +from qualtran.resource_counting import get_cost_value, QECGatesCost def test_examples(bloq_autotester): @@ -107,4 +105,6 @@ def test_t_complexity(): dtype = QUInt(n) bloq = ControlledAddOrSubtract(dtype, dtype) - assert get_cost_value(bloq, BloqCount.for_gateset('t')) == {TGate(): 4 * n - 4} + counts = get_cost_value(bloq, QECGatesCost()).total_t_and_ccz_count() + assert counts['n_t'] == 0, 'toffoli only' + assert counts['n_ccz'] == n - 1 diff --git a/qualtran/bloqs/arithmetic/permutation_test.py b/qualtran/bloqs/arithmetic/permutation_test.py index 06167d956..2414b97d6 100644 --- a/qualtran/bloqs/arithmetic/permutation_test.py +++ b/qualtran/bloqs/arithmetic/permutation_test.py @@ -37,9 +37,10 @@ Permutation, PermutationCycle, ) -from qualtran.bloqs.basic_gates import CNOT, TGate, XGate -from qualtran.bloqs.bookkeeping import Allocate, ArbitraryClifford, Free -from qualtran.resource_counting.generalizers import ignore_split_join +from qualtran.bloqs.basic_gates import CNOT, XGate +from qualtran.bloqs.bookkeeping import Allocate, Free +from qualtran.bloqs.mcmt import And +from qualtran.resource_counting.generalizers import generalize_cvs, ignore_split_join from qualtran.symbolics import ceil, log2, slen @@ -62,11 +63,14 @@ def test_permutation_cycle_unitary_and_call_graph(): bloq.tensor_contract(), np.array([[0, 0, 1, 0], [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) ) - _, sigma = bloq.call_graph(generalizer=ignore_split_join) + cv = sympy.Symbol('cv') + _, sigma = bloq.call_graph( + generalizer=[ignore_split_join, generalize_cvs], keep=lambda b: isinstance(b, And) + ) assert sigma == { CNOT(): 8, - TGate(): 16, - ArbitraryClifford(n=2): 76, + And(cv1=cv, cv2=cv): 4, + And(cv1=cv, cv2=cv).adjoint(): 4, Allocate(QBit()): 1, Free(QBit()): 1, } @@ -76,10 +80,10 @@ def test_permutation_cycle_symbolic_call_graph(): bloq = _permutation_cycle_symb() logN, L = ceil(log2(bloq.N)), slen(bloq.cycle) - _, sigma = bloq.call_graph() + _, sigma = bloq.call_graph(keep=lambda b: isinstance(b, And)) assert sigma == { - ArbitraryClifford(n=2): (L + 1) * (13 * logN - 13), - TGate(): (L + 1) * (4 * logN - 4), + And(): (L + 1) * (logN - 1), + And().adjoint(): (L + 1) * (logN - 1), CNOT(): L * logN + L + 1, } @@ -103,12 +107,12 @@ def test_permutation_unitary_and_call_graph(): ), ) - _, sigma = bloq.call_graph(generalizer=ignore_split_join) + _, sigma = bloq.call_graph(generalizer=ignore_split_join, keep=lambda b: isinstance(b, And)) assert sigma == { CNOT(): 17, - TGate(): 56, + And(): 56 // 4, + And().adjoint(): 56 // 4, XGate(): 56, - ArbitraryClifford(n=2): 182, Allocate(QBit()): 2, Free(QBit()): 2, } @@ -130,9 +134,9 @@ def test_permutation_symbolic_call_graph(): logN = ceil(log2(N)) bloq = _permutation_symb() - _, sigma = bloq.call_graph() + _, sigma = bloq.call_graph(keep=lambda b: isinstance(b, And)) assert sigma == { - ArbitraryClifford(n=2): (N + 1) * (13 * logN - 13), - TGate(): (N + 1) * (4 * logN - 4), + And().adjoint(): (N + 1) * (logN - 1), + And(): (N + 1) * (logN - 1), CNOT(): N * logN + N + 1, } diff --git a/qualtran/bloqs/chemistry/df/double_factorization_test.py b/qualtran/bloqs/chemistry/df/double_factorization_test.py index 8a2777380..5dd65346f 100644 --- a/qualtran/bloqs/chemistry/df/double_factorization_test.py +++ b/qualtran/bloqs/chemistry/df/double_factorization_test.py @@ -25,6 +25,7 @@ from qualtran.bloqs.state_preparation.prepare_uniform_superposition import ( PrepareUniformSuperposition, ) +from qualtran.resource_counting import get_cost_value, QECGatesCost from qualtran.testing import execute_notebook @@ -71,7 +72,7 @@ def test_compare_cost_to_openfermion(): num_bits_rot_aa_inner=7, num_bits_rot=num_bits_rot, ) - _, counts = bloq.call_graph() + t_counts = get_cost_value(bloq, QECGatesCost()).total_t_count() # https://github.com/quantumlib/OpenFermion/issues/839 of_cost = compute_cost( num_spin_orb, lambd, 1e-3, num_aux, num_eig, num_bits_state_prep, num_bits_rot, 10_000 @@ -83,16 +84,14 @@ def test_compare_cost_to_openfermion(): prog_rot_qrom_diff = 60 missing_toffoli = 4 # need one more toffoli for second application of CZ swap_cost = 4 * (7 - 4) * num_spin_orb // 2 - qual_cost = ( - counts[TGate()] - inner_prep_qrom_diff - prog_rot_qrom_diff + missing_toffoli - swap_cost - ) + qual_cost = t_counts - inner_prep_qrom_diff - prog_rot_qrom_diff + missing_toffoli - swap_cost # correct the expected cost by using a different uniform superposition algorithm # see: https://github.com/quantumlib/Qualtran/issues/611 eta = power_two(num_aux + 1) cost1a = 4 * 2 * (3 * nl - 3 * eta + 2 * 7 - 9) prep = PrepareUniformSuperposition(num_aux + 1) - cost1a_mod = prep.call_graph()[1][TGate()] - cost1a_mod += prep.adjoint().call_graph()[1][TGate()] + cost1a_mod = get_cost_value(prep, QECGatesCost()).total_t_count() + cost1a_mod += get_cost_value(prep.adjoint(), QECGatesCost()).total_t_count() delta_uni_prep = cost1a_mod - cost1a qual_cost -= delta_uni_prep inner_refl = num_bits_state_prep + 1 diff --git a/qualtran/bloqs/chemistry/df/prepare_test.py b/qualtran/bloqs/chemistry/df/prepare_test.py index 77e70efdc..3cde32748 100644 --- a/qualtran/bloqs/chemistry/df/prepare_test.py +++ b/qualtran/bloqs/chemistry/df/prepare_test.py @@ -27,6 +27,7 @@ from qualtran.bloqs.state_preparation.prepare_uniform_superposition import ( PrepareUniformSuperposition, ) +from qualtran.resource_counting import get_cost_value, QECGatesCost def test_prep_inner(bloq_autotester): @@ -48,13 +49,11 @@ def test_outerprep_t_counts(): outer_prep = OuterPrepareDoubleFactorization( num_aux, num_bits_state_prep=num_bits_state_prep, num_bits_rot_aa=num_bits_rot_aa ) - _, counts = outer_prep.call_graph() - toff = counts[TGate()] // 4 + toff = get_cost_value(outer_prep, QECGatesCost()).total_t_and_ccz_count()['n_ccz'] outer_prep = OuterPrepareDoubleFactorization( num_aux, num_bits_state_prep=num_bits_state_prep, num_bits_rot_aa=num_bits_rot_aa ).adjoint() - _, counts = outer_prep.call_graph() - toff += counts[TGate()] // 4 + toff += get_cost_value(outer_prep, QECGatesCost()).total_t_and_ccz_count()['n_ccz'] # The output size for the QROM for the first state preparation in Eq. (C27) eta = power_two(num_aux + 1) nl = num_aux.bit_length() @@ -65,8 +64,8 @@ def test_outerprep_t_counts(): # correct the expected cost by using a different uniform superposition algorithm # https://github.com/quantumlib/Qualtran/issues/611 prep = PrepareUniformSuperposition(num_aux + 1) - cost1a_mod = prep.call_graph()[1][TGate()] // 4 - cost1a_mod += prep.adjoint().call_graph()[1][TGate()] // 4 + cost1a_mod = get_cost_value(prep, QECGatesCost()).total_t_and_ccz_count()['n_ccz'] + cost1a_mod += get_cost_value(prep.adjoint(), QECGatesCost()).total_t_and_ccz_count()['n_ccz'] assert cost1a != cost1a_mod assert toff == cost1a_mod + cost1b + cost1cd @@ -107,8 +106,7 @@ def test_inner_prepare_t_counts(): num_bits_rot_aa=num_bits_rot_aa, num_bits_state_prep=num_bits_state_prep, ) - _, counts = in_prep.call_graph() - toff = counts[TGate()] // 4 + toff = get_cost_value(in_prep, QECGatesCost()).total_t_and_ccz_count()['n_ccz'] in_prep = InnerPrepareDoubleFactorization( num_aux=num_aux, num_spin_orb=num_spin_orb, @@ -116,8 +114,7 @@ def test_inner_prepare_t_counts(): num_bits_rot_aa=num_bits_rot_aa, num_bits_state_prep=num_bits_state_prep, ).adjoint() - _, counts = in_prep.call_graph() - toff += counts[TGate()] // 4 + toff += get_cost_value(in_prep, QECGatesCost()).total_t_and_ccz_count()['n_ccz'] toff *= 2 # cost is for the two applications of the in-prep, in-prep^ # application of ciruit. # captured from cost3 in openfermion df.compute_cost diff --git a/qualtran/bloqs/chemistry/pbc/first_quantization/prepare_nu_test.py b/qualtran/bloqs/chemistry/pbc/first_quantization/prepare_nu_test.py index 226e992bb..6cf67ef6f 100644 --- a/qualtran/bloqs/chemistry/pbc/first_quantization/prepare_nu_test.py +++ b/qualtran/bloqs/chemistry/pbc/first_quantization/prepare_nu_test.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from qualtran.bloqs.basic_gates import TGate from qualtran.bloqs.chemistry.pbc.first_quantization.prepare_nu import PrepareNuState +from qualtran.resource_counting import get_cost_value, QECGatesCost from qualtran.testing import assert_valid_bloq_decomposition @@ -37,11 +37,9 @@ def test_prepare_nu_t_counts(): eq_90 = 3 * num_bits_p**2 + 15 * num_bits_p - 7 + 4 * num_bits_m * (num_bits_p + 1) assert expected_cost == eq_90 + 5 prep = PrepareNuState(num_bits_p, m_param) - _, counts = prep.call_graph() - qual_cost = counts[TGate()] + qual_cost = get_cost_value(prep, QECGatesCost()).total_t_count() prep = PrepareNuState(num_bits_p, m_param).adjoint() - _, counts = prep.call_graph() - qual_cost += counts[TGate()] + qual_cost += get_cost_value(prep, QECGatesCost()).total_t_count() qual_cost //= 4 comp_diff = 1 assert qual_cost == expected_cost - comp_diff diff --git a/qualtran/bloqs/chemistry/pbc/first_quantization/prepare_uv_test.py b/qualtran/bloqs/chemistry/pbc/first_quantization/prepare_uv_test.py index 4c0d53ff9..266e2557b 100644 --- a/qualtran/bloqs/chemistry/pbc/first_quantization/prepare_uv_test.py +++ b/qualtran/bloqs/chemistry/pbc/first_quantization/prepare_uv_test.py @@ -14,11 +14,11 @@ import numpy as np -from qualtran.bloqs.basic_gates import TGate from qualtran.bloqs.chemistry.pbc.first_quantization.prepare_uv import ( _prepare_uv, PrepareUVFirstQuantization, ) +from qualtran.resource_counting import get_cost_value, QECGatesCost def test_prepare_uv(bloq_autotester): @@ -41,13 +41,11 @@ def test_prepare_uv_t_counts(): prep = PrepareUVFirstQuantization( num_bits_p, eta, num_atoms, m_param, lambda_zeta, num_bits_nuc_pos ) - _, counts = prep.call_graph() - qual_cost = counts[TGate()] + qual_cost = get_cost_value(prep, QECGatesCost()).total_t_count() prep = PrepareUVFirstQuantization( num_bits_p, eta, num_atoms, m_param, lambda_zeta, num_bits_nuc_pos ).adjoint() - _, counts = prep.call_graph() - qual_cost += counts[TGate()] + qual_cost += get_cost_value(prep, QECGatesCost()).total_t_count() qual_cost //= 4 comp_diff = 1 assert qual_cost == expected_cost - comp_diff diff --git a/qualtran/bloqs/chemistry/pbc/first_quantization/projectile/prepare_nu_test.py b/qualtran/bloqs/chemistry/pbc/first_quantization/projectile/prepare_nu_test.py index 6dceca0d8..c18e58d04 100644 --- a/qualtran/bloqs/chemistry/pbc/first_quantization/projectile/prepare_nu_test.py +++ b/qualtran/bloqs/chemistry/pbc/first_quantization/projectile/prepare_nu_test.py @@ -12,12 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from qualtran.bloqs.basic_gates import TGate from qualtran.bloqs.chemistry.pbc.first_quantization.projectile.prepare_nu import ( _prep_mu_proj, _prep_nu_proj, PrepareNuStateWithProj, ) +from qualtran.resource_counting import get_cost_value, QECGatesCost def test_prepare_num(bloq_autotester): @@ -44,11 +44,9 @@ def test_prepare_nu_with_proj_t_counts(): ) assert expected_cost == eq_c6 + 5 prep = PrepareNuStateWithProj(num_bits_p, num_bits_n, m_param) - _, counts = prep.call_graph() - qual_cost = counts[TGate()] + qual_cost = get_cost_value(prep, QECGatesCost()).total_t_count() prep = PrepareNuStateWithProj(num_bits_p, num_bits_n, m_param).adjoint() - _, counts = prep.call_graph() - qual_cost += counts[TGate()] + qual_cost += get_cost_value(prep, QECGatesCost()).total_t_count() qual_cost //= 4 comp_diff = 1 assert qual_cost == expected_cost - comp_diff diff --git a/qualtran/bloqs/chemistry/pbc/first_quantization/projectile/prepare_uv_test.py b/qualtran/bloqs/chemistry/pbc/first_quantization/projectile/prepare_uv_test.py index df22dcf2e..b516f4c5c 100644 --- a/qualtran/bloqs/chemistry/pbc/first_quantization/projectile/prepare_uv_test.py +++ b/qualtran/bloqs/chemistry/pbc/first_quantization/projectile/prepare_uv_test.py @@ -14,11 +14,11 @@ import numpy as np -from qualtran.bloqs.basic_gates import TGate from qualtran.bloqs.chemistry.pbc.first_quantization.projectile.prepare_uv import ( _prep_uv_proj, PrepareUVFirstQuantizationWithProj, ) +from qualtran.resource_counting import get_cost_value, QECGatesCost def test_prep_uv_proj(bloq_autotester): @@ -42,13 +42,11 @@ def test_prepare_uv_t_counts(): prep = PrepareUVFirstQuantizationWithProj( num_bits_p, num_bits_n, eta, num_atoms, m_param, lambda_zeta, num_bits_nuc_pos ) - _, counts = prep.call_graph() - qual_cost = counts[TGate()] + qual_cost = get_cost_value(prep, QECGatesCost()).total_t_count() prep = PrepareUVFirstQuantizationWithProj( num_bits_p, num_bits_n, eta, num_atoms, m_param, lambda_zeta, num_bits_nuc_pos ).adjoint() - _, counts = prep.call_graph() - qual_cost += counts[TGate()] + qual_cost += get_cost_value(prep, QECGatesCost()).total_t_count() qual_cost //= 4 comp_diff = 1 assert qual_cost == expected_cost - comp_diff diff --git a/qualtran/bloqs/chemistry/pbc/first_quantization/projectile/select_and_prepare_test.py b/qualtran/bloqs/chemistry/pbc/first_quantization/projectile/select_and_prepare_test.py index 636308fd4..74fd1646d 100644 --- a/qualtran/bloqs/chemistry/pbc/first_quantization/projectile/select_and_prepare_test.py +++ b/qualtran/bloqs/chemistry/pbc/first_quantization/projectile/select_and_prepare_test.py @@ -22,6 +22,7 @@ PrepareFirstQuantizationWithProj, SelectFirstQuantizationWithProj, ) +from qualtran.resource_counting import get_cost_value, QECGatesCost from qualtran.testing import assert_valid_bloq_decomposition, execute_notebook @@ -45,13 +46,12 @@ def test_select_t_costs(): num_atoms = 10 lambda_zeta = 10 num_bits_nuc_pos = 41 - cost = 0 sel_first_quant = SelectFirstQuantizationWithProj( num_bits_p, num_bits_n, eta, num_atoms, lambda_zeta, num_bits_nuc_pos=num_bits_nuc_pos ) assert_valid_bloq_decomposition(sel_first_quant) - cost += sel_first_quant.call_graph()[1][TGate()] + cost = get_cost_value(sel_first_quant, QECGatesCost()).total_t_count() # Swaps expected_cost = 7 * (12 * eta * num_bits_p + 6 * num_bits_n) + 4 * (4 * eta - 6) # diff --git a/qualtran/bloqs/chemistry/sparse/select_bloq_test.py b/qualtran/bloqs/chemistry/sparse/select_bloq_test.py index b645446cc..4afa77152 100644 --- a/qualtran/bloqs/chemistry/sparse/select_bloq_test.py +++ b/qualtran/bloqs/chemistry/sparse/select_bloq_test.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from qualtran.bloqs.basic_gates import TGate from qualtran.bloqs.chemistry.sparse.select_bloq import _sel_sparse @@ -22,6 +21,6 @@ def test_prep_inner(bloq_autotester): def test_decompose_bloq_counts(): sel = _sel_sparse() - cost_decomp = sel.decompose_bloq().call_graph()[1][TGate()] - cost_call = sel.call_graph()[1][TGate()] + cost_decomp = sel.decompose_bloq().call_graph()[1] + cost_call = sel.call_graph()[1] assert cost_call == cost_decomp diff --git a/qualtran/bloqs/chemistry/thc/thc.ipynb b/qualtran/bloqs/chemistry/thc/thc.ipynb index 05f0cc1ff..e5a8b65da 100644 --- a/qualtran/bloqs/chemistry/thc/thc.ipynb +++ b/qualtran/bloqs/chemistry/thc/thc.ipynb @@ -192,6 +192,7 @@ "from qualtran.resource_counting.classify_bloqs import classify_t_count_by_bloq_type\n", "\n", "binned_counts = classify_t_count_by_bloq_type(thc_uni)\n", + "\n", "# number of bits for mu register (nm in THC paper)\n", "# note this register should range up to num_mu + 1, not num_mu, hence it's just bit_length not (num_mu - 1).bit_length()\n", "nm = thc_uni.num_mu.bit_length()\n", @@ -199,12 +200,12 @@ "# The factor of 4 is for Toffoli -> T conversion\n", "paper_costs = {\n", " 'arithmetic': 4*(4*(nm - 1) + (4*nm - 3)), # 4 comparitors of cost nm - 1 Toffolis\n", - " 'rotation': 4*(4 + 4), # Given as br - 3, br = 7 is the number of bits of precision for rotations.\n", + " 'rotations': 4*(4 + 4), # Given as br - 3, br = 7 is the number of bits of precision for rotations.\n", " 'reflection': 4*(3 + 2*nm-1), # 5 qubit reflection for comparitors and 2*nm + 1 qubits reflect after hadamards\n", " 'other': 4*3, # \"Checking the inequality test\" unclear if this is the multi-control not gate.\n", "}\n", - "for k, v in paper_costs.items():\n", - " print(f\"{k+':':15s} qualtran = {binned_counts[k]:5d} vs paper cost = {v:5d}.\")\n", + "for k in (paper_costs.keys() | binned_counts.keys()):\n", + " print(f\"{k+':':15s} qualtran = {binned_counts.get(k,0):5d} vs paper cost = {paper_costs.get(k,0):5d}.\")\n", "\n", "assert binned_counts['arithmetic'] == 276" ] diff --git a/qualtran/bloqs/data_loading/qrom_test.py b/qualtran/bloqs/data_loading/qrom_test.py index 69b716735..271346c34 100644 --- a/qualtran/bloqs/data_loading/qrom_test.py +++ b/qualtran/bloqs/data_loading/qrom_test.py @@ -19,18 +19,13 @@ import pytest import sympy +import qualtran.testing as qlt_testing from qualtran import QUInt from qualtran._infra.gate_with_registers import split_qubits, total_bits -from qualtran.bloqs.basic_gates import CNOT, TGate from qualtran.bloqs.data_loading.qrom import _qrom_multi_data, _qrom_multi_dim, _qrom_small, QROM from qualtran.cirq_interop.t_complexity_protocol import t_complexity from qualtran.cirq_interop.testing import assert_circuit_inp_out_cirqsim, GateHelper -from qualtran.resource_counting.generalizers import cirq_to_bloqs -from qualtran.testing import ( - assert_valid_bloq_decomposition, - assert_wire_symbols_match_expected, - execute_notebook, -) +from qualtran.resource_counting import get_cost_value, QECGatesCost def test_qrom_small(bloq_autotester): @@ -58,7 +53,7 @@ def test_qrom_multi_dim(bloq_autotester): ) def test_qrom_1d_full(data, num_controls: int): qrom = QROM.build_from_data(*data, num_controls=num_controls) - assert_valid_bloq_decomposition(qrom) + qlt_testing.assert_valid_bloq_decomposition(qrom) greedy_mm = cirq.GreedyQubitManager('a', maximize_reuse=True) g = GateHelper(qrom, context=cirq.DecompositionContext(greedy_mm)) @@ -209,7 +204,7 @@ def test_qrom_diagram(): @pytest.mark.notebook def test_notebook(): - execute_notebook('qrom') + qlt_testing.execute_notebook('qrom') @pytest.mark.parametrize( @@ -319,16 +314,18 @@ def test_qrom_variable_spacing(): def test_qrom_wire_symbols(): qrom = QROM.build_from_data([3, 3, 3, 3]) - assert_wire_symbols_match_expected(qrom, ['In', 'QROM_a']) + qlt_testing.assert_wire_symbols_match_expected(qrom, ['In', 'QROM_a']) qrom = QROM.build_from_data([3, 3, 3, 3], [2, 2, 2, 2]) - assert_wire_symbols_match_expected(qrom, ['In', 'QROM_a', 'QROM_b']) + qlt_testing.assert_wire_symbols_match_expected(qrom, ['In', 'QROM_a', 'QROM_b']) qrom = QROM.build_from_data([[3, 3], [3, 3]], [[2, 2], [2, 2]], [[1, 1], [2, 2]]) - assert_wire_symbols_match_expected(qrom, ['In_i', 'In_j', 'QROM_a', 'QROM_b', 'QROM_c']) + qlt_testing.assert_wire_symbols_match_expected( + qrom, ['In_i', 'In_j', 'QROM_a', 'QROM_b', 'QROM_c'] + ) qrom = QROM.build_from_data(np.arange(27).reshape(3, 3, 3)) - assert_wire_symbols_match_expected(qrom, ['In_i', 'In_j', 'In_k', 'QROM_a']) + qlt_testing.assert_wire_symbols_match_expected(qrom, ['In_i', 'In_j', 'In_k', 'QROM_a']) @pytest.mark.slow @@ -359,7 +356,7 @@ def test_qrom_multi_dim_full(data, num_controls): target_bitsizes=target_bitsizes, num_controls=num_controls, ) - assert_valid_bloq_decomposition(qrom) + qlt_testing.assert_valid_bloq_decomposition(qrom) greedy_mm = cirq.GreedyQubitManager('a', maximize_reuse=True) g = GateHelper(qrom, context=cirq.DecompositionContext(greedy_mm)) @@ -416,25 +413,22 @@ def test_qrom_call_graph_matches_decomposition(num_controls): # Base case arr = np.arange(50) qrom = QROM.build_from_data(arr, num_controls=num_controls) - _, sigma_call = qrom.call_graph(generalizer=cirq_to_bloqs) - _, sigma_dcmp = qrom.decompose_bloq().call_graph(generalizer=cirq_to_bloqs) - assert sigma_call[TGate()] == sigma_dcmp[TGate()] - assert sigma_call[CNOT()] == sigma_dcmp[CNOT()] + cost_call = get_cost_value(qrom, QECGatesCost()) + cost_dcmp = get_cost_value(qrom.decompose_bloq(), QECGatesCost()) + assert cost_call.total_t_and_ccz_count() == cost_dcmp.total_t_and_ccz_count() # Multiple Multi dimensional arrays arr_a = np.arange(64).reshape(8, 8) arr_b = 10 * np.arange(64).reshape(8, 8) qrom = QROM.build_from_data(arr_a, arr_b, num_controls=num_controls) - _, sigma_call = qrom.call_graph(generalizer=cirq_to_bloqs) - _, sigma_dcmp = qrom.decompose_bloq().call_graph(generalizer=cirq_to_bloqs) - assert sigma_call[TGate()] == sigma_dcmp[TGate()] - assert sigma_call[CNOT()] == sigma_dcmp[CNOT()] + cost_call = get_cost_value(qrom, QECGatesCost()) + cost_dcmp = get_cost_value(qrom.decompose_bloq(), QECGatesCost()) + assert cost_call.total_t_and_ccz_count() == cost_dcmp.total_t_and_ccz_count() # Variable QROM case. arr_a = np.array([1, 2, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5]) arr_b = 10 * arr_a qrom = QROM.build_from_data(arr_a, arr_b, num_controls=num_controls) - _, sigma_call = qrom.call_graph(generalizer=cirq_to_bloqs) - _, sigma_dcmp = qrom.decompose_bloq().call_graph(generalizer=cirq_to_bloqs) - assert sigma_call[TGate()] == sigma_dcmp[TGate()] - assert sigma_call[CNOT()] == sigma_dcmp[CNOT()] + cost_call = get_cost_value(qrom, QECGatesCost()) + cost_dcmp = get_cost_value(qrom.decompose_bloq(), QECGatesCost()) + assert cost_call.total_t_and_ccz_count() == cost_dcmp.total_t_and_ccz_count() diff --git a/qualtran/bloqs/mcmt/and_bloq.ipynb b/qualtran/bloqs/mcmt/and_bloq.ipynb index 63e58786e..8208ef807 100644 --- a/qualtran/bloqs/mcmt/and_bloq.ipynb +++ b/qualtran/bloqs/mcmt/and_bloq.ipynb @@ -143,6 +143,36 @@ "show_counts_sigma(and_bloq_sigma)" ] }, + { + "cell_type": "markdown", + "id": "76e25f29-0a5f-41b1-ba2f-e61cfdd2ebe1", + "metadata": {}, + "source": [ + "### Clifford + T implementation\n", + "\n", + "`And` will be considered an Atomic (non-decomposable) bloq within the Qualtran standard library. An additional method is provided to assist in fully compiling to a Clifford+T gateset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4912636d-40aa-4db9-a383-88e77e8b5e14", + "metadata": {}, + "outputs": [], + "source": [ + "and_bloq.to_clifford_t_circuit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22496f4b-9bab-439e-bdf1-e10dc95c316d", + "metadata": {}, + "outputs": [], + "source": [ + "and_bloq.adjoint().to_clifford_t_circuit()" + ] + }, { "cell_type": "markdown", "id": "50800962", @@ -382,7 +412,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/qualtran/bloqs/mcmt/and_bloq.py b/qualtran/bloqs/mcmt/and_bloq.py index 35e8d4acb..87e1399d0 100644 --- a/qualtran/bloqs/mcmt/and_bloq.py +++ b/qualtran/bloqs/mcmt/and_bloq.py @@ -38,6 +38,7 @@ BloqDocSpec, CompositeBloq, ConnectionT, + DecomposeTypeError, GateWithRegisters, QBit, Register, @@ -47,6 +48,7 @@ from qualtran.bloqs.basic_gates import TGate, XGate from qualtran.bloqs.bookkeeping import ArbitraryClifford from qualtran.cirq_interop import decompose_from_cirq_style_method +from qualtran.cirq_interop.t_complexity_protocol import TComplexity from qualtran.drawing import Circle, directional_text_box, Text, WireSymbol from qualtran.resource_counting import big_O, BloqCountT, SympySymbolAllocator from qualtran.resource_counting.generalizers import ( @@ -62,6 +64,9 @@ if TYPE_CHECKING: import quimb.tensor as qtn +# TODO: https://github.com/quantumlib/Qualtran/issues/1346 +FLAG_AND_AS_LEAF = False + @frozen class And(GateWithRegisters): @@ -98,7 +103,15 @@ def signature(self) -> Signature: def adjoint(self) -> 'And': return attrs.evolve(self, uncompute=not self.uncompute) + def decompose_bloq(self) -> 'CompositeBloq': + if FLAG_AND_AS_LEAF: + raise DecomposeTypeError(f"{self} is atomic.") + return decompose_from_cirq_style_method(self) + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> Set['BloqCountT']: + if FLAG_AND_AS_LEAF: + raise DecomposeTypeError(f"{self} is atomic.") + if isinstance(self.cv1, sympy.Expr) or isinstance(self.cv2, sympy.Expr): pre_post_cliffords: Union[sympy.Order, int] = big_O(1) else: @@ -108,6 +121,19 @@ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> Set['BloqCountT']: return {(ArbitraryClifford(n=2), 9 + 2 * pre_post_cliffords), (TGate(), 4)} + def _t_complexity_(self) -> 'TComplexity': + if not FLAG_AND_AS_LEAF: + return NotImplemented + + if isinstance(self.cv1, sympy.Expr) or isinstance(self.cv2, sympy.Expr): + pre_post_cliffords: Union[sympy.Order, int] = 0 + else: + pre_post_cliffords = 2 - self.cv1 - self.cv2 + if self.uncompute: + return TComplexity(clifford=4 + 2 * pre_post_cliffords) + + return TComplexity(t=4, clifford=9 + 2 * pre_post_cliffords) + def on_classical_vals( self, *, ctrl: NDArray[np.uint8], target: Optional[int] = None ) -> Dict[str, ClassicalValT]: @@ -195,6 +221,41 @@ def decompose_from_registers( yield [cirq.H(target), cirq.S(target)] yield pre_post_ops + def to_clifford_t_circuit(self) -> 'cirq.FrozenCircuit': + """Decomposes a single `And` gate on 2 controls and 1 target in terms of Clifford+T gates. + + * And(cv).on(c1, c2, target) uses 4 T-gates and assumes target is in |0> state. + * And(cv, adjoint=True).on(c1, c2, target) uses measurement based un-computation + (0 T-gates) and will always leave the target in |0> state. + """ + c1 = cirq.NamedQubit('ctrl_0') + c2 = cirq.NamedQubit('ctrl_1') + target = cirq.NamedQubit('target') + pre_post_ops = [cirq.X(q) for (q, v) in zip([c1, c2], [self.cv1, self.cv2]) if v == 0] + circuit = cirq.Circuit(pre_post_ops) + if self.uncompute: + circuit += cirq.Circuit( + [ + cirq.H(target), + cirq.measure(target, key=f"{target}"), + cirq.CZ(c1, c2).with_classical_controls(f"{target}"), + cirq.reset(target), + ] + ) + else: + circuit += cirq.Circuit( + [ + [cirq.H(target), cirq.T(target)], + [cirq.CNOT(c1, target), cirq.CNOT(c2, target)], + [cirq.CNOT(target, c1), cirq.CNOT(target, c2)], + [cirq.T(c1) ** -1, cirq.T(c2) ** -1, cirq.T(target)], + [cirq.CNOT(target, c1), cirq.CNOT(target, c2)], + [cirq.H(target), cirq.S(target)], + ] + ) + circuit += pre_post_ops + return circuit.freeze() + def __pow__(self, power: int) -> 'And': if power == 1: return self diff --git a/qualtran/resource_counting/_costing.py b/qualtran/resource_counting/_costing.py index da866d024..4200b0aca 100644 --- a/qualtran/resource_counting/_costing.py +++ b/qualtran/resource_counting/_costing.py @@ -29,6 +29,8 @@ Union, ) +from qualtran import CompositeBloq + from ._generalization import _make_composite_generalizer, GeneralizerT if TYPE_CHECKING: @@ -122,7 +124,7 @@ def _get_cost_value( return cost_key.zero() # Strategy 1: Use cached value - if bloq in costs_cache: + if not isinstance(bloq, CompositeBloq) and bloq in costs_cache: logger.debug("Using cached %s for %s", cost_key, bloq) return costs_cache[bloq] @@ -144,7 +146,8 @@ def _get_cost_val_internal(callee: 'Bloq'): computed_cost = cost_key.compute(bloq, _get_cost_val_internal) tdur = time.perf_counter() - tstart logger.info("Computed %s for %s in %g s", cost_key, bloq, tdur) - costs_cache[bloq] = computed_cost + if not isinstance(bloq, CompositeBloq): + costs_cache[bloq] = computed_cost return computed_cost diff --git a/qualtran/resource_counting/classify_bloqs.py b/qualtran/resource_counting/classify_bloqs.py index f5efb774b..60ab826e6 100644 --- a/qualtran/resource_counting/classify_bloqs.py +++ b/qualtran/resource_counting/classify_bloqs.py @@ -24,7 +24,6 @@ ignore_cliffords, ignore_split_join, ) -from qualtran.resource_counting.t_counts_from_sigma import t_counts_from_sigma from qualtran.symbolics import is_symbolic if TYPE_CHECKING: @@ -88,6 +87,8 @@ def classify_t_count_by_bloq_type( Returns classified_bloqs: dictionary containing the T count for different types of bloqs. """ + from qualtran.resource_counting import get_cost_value, QECGatesCost + if bloq_classification is None: bloq_classification = _get_basic_bloq_classification() keeper = lambda bloq: classify_bloq(bloq, bloq_classification) != 'other' @@ -105,10 +106,10 @@ def classify_t_count_by_bloq_type( classified_bloqs: Dict[str, Union[int, sympy.Expr]] = defaultdict(int) for k, v in sigma.items(): classification = classify_bloq(k, bloq_classification) - t_counts = t_counts_from_sigma(k.call_graph()[1]) + t_counts = get_cost_value(k, QECGatesCost()).total_t_count() if t_counts > 0: classified_bloqs[classification] += v * t_counts - return classified_bloqs + return dict(classified_bloqs) _CLIFFORD_ANGLES = np.array( diff --git a/qualtran/resource_counting/classify_bloqs_test.py b/qualtran/resource_counting/classify_bloqs_test.py index 5a8076573..dc0ae3db1 100644 --- a/qualtran/resource_counting/classify_bloqs_test.py +++ b/qualtran/resource_counting/classify_bloqs_test.py @@ -30,7 +30,7 @@ from qualtran.bloqs.state_preparation.prepare_uniform_superposition import ( PrepareUniformSuperposition, ) -from qualtran.resource_counting import BloqCountT +from qualtran.resource_counting import BloqCountT, get_cost_value, QECGatesCost from qualtran.resource_counting.classify_bloqs import ( _get_basic_bloq_classification, bloq_is_rotation, @@ -38,7 +38,6 @@ classify_bloq, classify_t_count_by_bloq_type, ) -from qualtran.resource_counting.t_counts_from_sigma import t_counts_from_sigma if TYPE_CHECKING: from qualtran.resource_counting import BloqCountT, SympySymbolAllocator @@ -74,7 +73,9 @@ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> Set['BloqCountT']: def test_default_classification(bloq_count, classification): bloq = TestBundleOfBloqs(bloq_count) classified_bloqs = classify_t_count_by_bloq_type(bloq) - assert classified_bloqs[classification] == t_counts_from_sigma(bloq.call_graph()[1]) + assert classified_bloqs == { + classification: get_cost_value(bloq, QECGatesCost()).total_t_count() + } def test_dont_return_zeros(): @@ -110,7 +111,9 @@ def test_classify_bloq_counts_with_custom_bloq_classification(): test_bloq, bloq_classification=bloq_classification ) assert classified_bloqs == {'swaps': 42 * 10 * 7, 'other': 3 * 4 * (4 - 1)} - assert test_bloq.call_graph()[1].get(TGate()) == sum(classified_bloqs.values()) + assert get_cost_value(test_bloq, QECGatesCost()).total_t_count() == sum( + classified_bloqs.values() + ) def test_bloq_is_rotation(): diff --git a/qualtran/testing.py b/qualtran/testing.py index d60016c79..54d0daa51 100644 --- a/qualtran/testing.py +++ b/qualtran/testing.py @@ -499,8 +499,8 @@ def assert_equivalent_bloq_example_counts(bloq_ex: BloqExample) -> None: only_decomp = set(decomp_counts.keys()) - set(manual_counts.keys()) if only_decomp: msg.append(f"Bloq's missing from annotation: {only_decomp}") - msg.append(f'Annotation: {manual_counts}') - msg.append(f'Decomp: {decomp_counts}') + msg.append(f'Annotation: {sorted(manual_counts.items(), key=str)}') + msg.append(f'Decomp: {sorted(decomp_counts.items(), key=str)}') raise BloqCheckException.fail('\n'.join(msg)) assert has_manual_counts or has_decomp_counts