From 0ae665c851da12a6c7b8facb190575be9f22cb02 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 6 Aug 2021 03:37:44 +0000 Subject: [PATCH 01/10] Boolean Hamiltonian gate yields fewer gates --- cirq-core/cirq/ops/boolean_hamiltonian.py | 179 +++++++++++++++++- .../cirq/ops/boolean_hamiltonian_test.py | 95 ++++++++++ 2 files changed, 269 insertions(+), 5 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index c5a3de0776b..d8339600c57 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -20,6 +20,8 @@ [2] https://www.youtube.com/watch?v=AOKM9BkweVU is a useful intro [3] https://github.com/rsln-s/IEEE_QW_2020/blob/master/Slides.pdf """ +import itertools +import functools from typing import cast, Any, Dict, Generator, List, Sequence, Tuple @@ -106,6 +108,171 @@ def _decompose_(self): ) +def _gray_code_comparator(k1: Tuple[int, ...], k2: Tuple[int, ...], flip: bool = False) -> int: + """Compares two Gray-encoded binary numbers. + + Args: + k1: A tuple of ints, representing the bits that are one. For example, 6 would be (1, 2). + k2: The second number, represented similarly as k1. + flip: Whether to flip the comparison. + + Returns: + -1 if k1 < k2 (or +1 if flip is true) + 0 if k1 == k2 + +1 if k1 > k2 (or -1 if flip is true) + """ + max_1 = k1[-1] if k1 else -1 + max_2 = k2[-1] if k2 else -1 + if max_1 != max_2: + return -1 if (max_1 < max_2) ^ flip else 1 + if max_1 == -1: + return 0 + return _gray_code_comparator(k1[0:-1], k2[0:-1], not flip) + + +def _simplify_commuting_cnots( + cnots: List[Tuple[int, int]], flip_control_and_target: bool +) -> Tuple[bool, List[Tuple[int, int]]]: + """Attempts to commute CNOTs and remove cancelling pairs. + + Commutation relations are based on 9 (flip_control_and_target=False) or 10 + (flip_control_target=True) of [4]: + When flip_control_target=True: + + CNOT(j, i) @ CNOT(j, k) = CNOT(j, k) @ CNOT(j, i) + ───X─────── ───────X─── + │ │ + ───@───@─── = ───@───@─── + │ │ + ───────X─── ───X─────── + + When flip_control_target=False: + + CNOT(i, j) @ CNOT(k, j) = CNOT(k, j) @ CNOT(i, j) + ───@─────── ───────@─── + │ │ + ───X───X─── = ───X───X─── + │ │ + ───────@─── ───@─────── + + Args: + cnots: A list of CNOTS, encoded as integer tuples (control, target). + flip_control_and_target: Whether to flip control and target. + + Returns: + A Boolean that tells whether a simplification has been performed. + The CNOT list, potentially simplified. + """ + + target, control = (0, 1) if flip_control_and_target else (1, 0) + + i = 0 + qubit_to_index: Dict[int, int] = {cnots[i][control]: i} if cnots else {} + for j in range(1, len(cnots)): + if cnots[i][target] != cnots[j][target]: + # The targets (resp. control) don't match, so we reset the search. + i = j + qubit_to_index = {cnots[j][control]: j} + continue + + if cnots[j][control] in qubit_to_index: + k = qubit_to_index[cnots[j][control]] + # The controls (resp. targets) are the same, so we can simplify away. + cnots = [cnots[n] for n in range(len(cnots)) if n != j and n != k] + return True, cnots + + qubit_to_index[cnots[j][control]] = j + + return False, cnots + + +def _simplify_cnots_triplets( + cnots: List[Tuple[int, int]], flip_control_and_target: bool +) -> Tuple[bool, List[Tuple[int, int]]]: + """Simplifies CNOT pairs according to equation 11 of [4]. + + CNOT(i, j) @ CNOT(j, k) == CNOT(j, k) @ CNOT(i, k) @ CNOT(i, j) + ───@─────── ───────@───@─── + │ │ │ + ───X───@─── = ───@───┼───X─── + │ │ │ + ───────X─── ───X───X─────── + + Args: + cnots: A list of CNOTS, encoded as integer tuples (control, target). + flip_control_and_target: Whether to flip control and target. + + Returns: + A Boolean that tells whether a simplification has been performed. + The CNOT list, potentially simplified. + """ + target, control = (0, 1) if flip_control_and_target else (1, 0) + + # We investigate potential pivots sequentially. + for j in range(1, len(cnots) - 1): + # First, we look back for as long as the targets (resp. controls) are the same. + # They all commute, so all are potential candidates for being simplified. + common_a: Dict[int, int] = {} + for i in range(j - 1, -1, -1): + if cnots[i][control] != cnots[j][control]: + break + # We take a note of the control (resp. target). + common_a[cnots[i][target]] = i + + # Next, we look forward for as long as the controls (resp. targets) are the + # same. They all commute, so all are potential candidates for being simplified. + common_b: Dict[int, int] = {} + for k in range(j + 1, len(cnots)): + if cnots[j][target] != cnots[k][target]: + break + # We take a note of the target (resp. control). + common_b[cnots[k][control]] = k + + # Among all the candidates, find if they have a match. + keys = common_a.keys() & common_b.keys() + for key in keys: + assert common_a[key] != common_b[key] + # We perform the swap which removes the pivot. + new_idx: List[int] = ( + [idx for idx in range(0, j) if idx != common_a[key]] + + [common_b[key], common_a[key]] + + [idx for idx in range(j + 1, len(cnots)) if idx != common_b[key]] + ) + # Since we removed the pivot, the length should be one fewer. + assert len(new_idx) == len(cnots) - 1 + cnots = [cnots[idx] for idx in new_idx] + return True, cnots + + return False, cnots + + +def _simplify_cnots(cnots: List[Tuple[int, int]]) -> List[Tuple[int, int]]: + """Takes a series of CNOTs and tries to applies rule to cancel out gates. + + Algorithm based on "Efficient quantum circuits for diagonal unitaries without ancillas" by + Jonathan Welch, Daniel Greenbaum, Sarah Mostame, Alán Aspuru-Guzik + https://arxiv.org/abs/1306.3991 + + Args: + cnots: A list of CNOTs represented as tuples of integer (control, target). + + Returns: + A Boolean saying whether a simplification has been found. + The simplified list of CNOTs. + """ + + found_simplification = True + while found_simplification: + for simplify_fn, flip_control_and_target in itertools.product( + [_simplify_commuting_cnots, _simplify_cnots_triplets], [False, True] + ): + found_simplification, cnots = simplify_fn(cnots, flip_control_and_target) + if found_simplification: + break + + return cnots + + def _get_gates_from_hamiltonians( hamiltonian_polynomial_list: List['cirq.PauliSum'], qubit_map: Dict[str, 'cirq.Qid'], @@ -139,16 +306,18 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): cnots.extend((prevh[i], prevh[-1]) for i in range(len(prevh) - 1)) cnots.extend((currh[i], currh[-1]) for i in range(len(currh) - 1)) - # TODO(tonybruguier): At this point, some CNOT gates can be cancelled out according to: - # "Efficient quantum circuits for diagonal unitaries without ancillas" by Jonathan Welch, - # Daniel Greenbaum, Sarah Mostame, Alán Aspuru-Guzik - # https://arxiv.org/abs/1306.3991 + cnots = _simplify_cnots(cnots) for gate in (cirq.CNOT(qubits[c], qubits[t]) for c, t in cnots): yield gate + sorted_hamiltonian_keys = sorted( + hamiltonians.keys(), key=functools.cmp_to_key(_gray_code_comparator) + ) + previous_h: Tuple[int, ...] = () - for h, w in hamiltonians.items(): + for h in sorted_hamiltonian_keys: + w = hamiltonians[h] yield _apply_cnots(previous_h, h) if len(h) >= 1: diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index c23a01ad7b9..d1328aeaf41 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -11,14 +11,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import functools import itertools import math +import random import numpy as np import pytest import sympy.parsing.sympy_parser as sympy_parser import cirq +import cirq.ops.boolean_hamiltonian as bh @pytest.mark.parametrize( @@ -83,3 +86,95 @@ def test_circuit(boolean_str): # Compare the two: np.testing.assert_array_equal(actual, expected) + + +@pytest.mark.parametrize( + 'n_bits,expected_hs', + [ + (1, [(), (0,)]), + (2, [(), (0,), (0, 1), (1,)]), + (3, [(), (0,), (0, 1), (1,), (1, 2), (0, 1, 2), (0, 2), (2,)]), + ], +) +def test_gray_code_sorting(n_bits, expected_hs): + hs = [] + for x in range(2 ** n_bits): + h = [] + for i in range(n_bits): + if x % 2 == 1: + h.append(i) + x -= 1 + x //= 2 + hs.append(tuple(sorted(h))) + random.shuffle(hs) + + sorted_hs = sorted(list(hs), key=functools.cmp_to_key(bh._gray_code_comparator)) + + np.testing.assert_array_equal(sorted_hs, expected_hs) + + +@pytest.mark.parametrize( + 'seq_a,seq_b,expected', + [ + ((), (), 0), + ((), (0,), -1), + ((0,), (), 1), + ((0,), (0,), 0), + ], +) +def test_gray_code_comparison(seq_a, seq_b, expected): + assert bh._gray_code_comparator(seq_a, seq_b) == expected + + +@pytest.mark.parametrize( + 'input_cnots,input_flip_control_and_target,expected_simplified,expected_output_cnots', + [ + # Empty inputs don't get simplified. + ([], False, False, []), + ([], True, False, []), + # Single CNOTs don't get simplified. + ([(0, 1)], False, False, [(0, 1)]), + ([(0, 1)], True, False, [(0, 1)]), + # Simplify away two CNOTs that are identical: + ([(0, 1), (0, 1)], False, True, []), + ([(0, 1), (0, 1)], True, True, []), + # Also simplify away if there's another CNOT in between. + ([(0, 1), (2, 1), (0, 1)], False, True, [(2, 1)]), + ([(0, 1), (0, 2), (0, 1)], True, True, [(0, 2)]), + # However, the in-between has to share the same target/control. + ([(0, 1), (0, 2), (0, 1)], False, False, [(0, 1), (0, 2), (0, 1)]), + ([(0, 1), (2, 1), (0, 1)], True, False, [(0, 1), (2, 1), (0, 1)]), + ], +) +def test_simplify_commuting_cnots( + input_cnots, input_flip_control_and_target, expected_simplified, expected_output_cnots +): + actual_simplified, actual_output_cnots = bh._simplify_commuting_cnots( + input_cnots, input_flip_control_and_target + ) + assert actual_simplified == expected_simplified + assert actual_output_cnots == expected_output_cnots + + +@pytest.mark.parametrize( + 'input_cnots,input_flip_control_and_target,expected_simplified,expected_output_cnots', + [ + # Empty inputs don't get simplified. + ([], False, False, []), + ([], True, False, []), + # Single CNOTs don't get simplified. + ([(0, 1)], False, False, [(0, 1)]), + ([(0, 1)], True, False, [(0, 1)]), + # Simplify according to equation 11 of [4]. + ([(2, 1), (2, 0), (1, 0)], False, True, [(1, 0), (2, 1)]), + ([(1, 2), (0, 2), (0, 1)], True, True, [(0, 1), (1, 2)]), + ], +) +def test_simplify_cnots_triplets( + input_cnots, input_flip_control_and_target, expected_simplified, expected_output_cnots +): + actual_simplified, actual_output_cnots = bh._simplify_cnots_triplets( + input_cnots, input_flip_control_and_target + ) + assert actual_simplified == expected_simplified + assert actual_output_cnots == expected_output_cnots From 61ea6453c1fb0095bc4e51f0bf29e105ab4768ea Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 18 Sep 2021 00:02:21 +0000 Subject: [PATCH 02/10] Address some of the comments --- cirq-core/cirq/ops/boolean_hamiltonian.py | 35 ++++++++++--------- .../cirq/ops/boolean_hamiltonian_test.py | 20 ++++++++--- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 015c99a8b84..afa384b1307 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -19,6 +19,8 @@ by Stuart Hadfield, https://arxiv.org/pdf/1804.09130.pdf [2] https://www.youtube.com/watch?v=AOKM9BkweVU is a useful intro [3] https://github.com/rsln-s/IEEE_QW_2020/blob/master/Slides.pdf +[4] Efficient Quantum Circuits for Diagonal Unitaries Without Ancillas by Jonathan Welch, Daniel + Greenbaum, Sarah Mostame, and Alán Aspuru-Guzik, https://arxiv.org/abs/1306.3991 """ import itertools import functools @@ -162,7 +164,11 @@ def _simplify_commuting_cnots( ───────@─── ───@─────── Args: - cnots: A list of CNOTS, encoded as integer tuples (control, target). + cnots: A list of CNOTS, encoded as integer tuples (control, target). The code does not make + any assumption as to the order of the CNOTs, but it is likely to work better if its + inputs are from Gray-sorted Hamiltonians. Regardless of the order of the CNOTs, the + code is conservative and should be robust to mis-ordered inputs with the only side + effect being a lack of simplification. flip_control_and_target: Whether to flip control and target. Returns: @@ -210,42 +216,40 @@ def _simplify_cnots_triplets( Returns: A Boolean that tells whether a simplification has been performed. - The CNOT list, potentially simplified. + The CNOT list, potentially simplified, encoded as integer tuples (control, target). """ target, control = (0, 1) if flip_control_and_target else (1, 0) # We investigate potential pivots sequentially. for j in range(1, len(cnots) - 1): - # First, we look back for as long as the targets (resp. controls) are the same. + # First, we look back for as long as the controls (resp. targets) are the same. # They all commute, so all are potential candidates for being simplified. - common_a: Dict[int, int] = {} + prev_match_index: Dict[int, int] = {} for i in range(j - 1, -1, -1): if cnots[i][control] != cnots[j][control]: break # We take a note of the control (resp. target). - common_a[cnots[i][target]] = i + prev_match_index[cnots[i][target]] = i - # Next, we look forward for as long as the controls (resp. targets) are the + # Next, we look forward for as long as the targets (resp. controls) are the # same. They all commute, so all are potential candidates for being simplified. - common_b: Dict[int, int] = {} + post_match_index: Dict[int, int] = {} for k in range(j + 1, len(cnots)): if cnots[j][target] != cnots[k][target]: break # We take a note of the target (resp. control). - common_b[cnots[k][control]] = k + post_match_index[cnots[k][control]] = k # Among all the candidates, find if they have a match. - keys = common_a.keys() & common_b.keys() + keys = prev_match_index.keys() & post_match_index.keys() for key in keys: - assert common_a[key] != common_b[key] # We perform the swap which removes the pivot. new_idx: List[int] = ( - [idx for idx in range(0, j) if idx != common_a[key]] - + [common_b[key], common_a[key]] - + [idx for idx in range(j + 1, len(cnots)) if idx != common_b[key]] + [idx for idx in range(0, j) if idx != prev_match_index[key]] + + [post_match_index[key], prev_match_index[key]] + + [idx for idx in range(j + 1, len(cnots)) if idx != post_match_index[key]] ) # Since we removed the pivot, the length should be one fewer. - assert len(new_idx) == len(cnots) - 1 cnots = [cnots[idx] for idx in new_idx] return True, cnots @@ -263,8 +267,7 @@ def _simplify_cnots(cnots: List[Tuple[int, int]]) -> List[Tuple[int, int]]: cnots: A list of CNOTs represented as tuples of integer (control, target). Returns: - A Boolean saying whether a simplification has been found. - The simplified list of CNOTs. + The simplified list of CNOTs, encoded as integer tuples (control, target). """ found_simplification = True diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index dca3f59b96f..b59e125907e 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -112,7 +112,7 @@ def test_with_custom_names(): ], ) def test_gray_code_sorting(n_bits, expected_hs): - hs = [] + hs_template = [] for x in range(2 ** n_bits): h = [] for i in range(n_bits): @@ -120,12 +120,17 @@ def test_gray_code_sorting(n_bits, expected_hs): h.append(i) x -= 1 x //= 2 - hs.append(tuple(sorted(h))) - random.shuffle(hs) + hs_template.append(tuple(sorted(h))) - sorted_hs = sorted(list(hs), key=functools.cmp_to_key(bh._gray_code_comparator)) + for seed in range(10): + random.seed(seed) - np.testing.assert_array_equal(sorted_hs, expected_hs) + hs = hs_template.copy() + random.shuffle(hs) + + sorted_hs = sorted(list(hs), key=functools.cmp_to_key(bh._gray_code_comparator)) + + np.testing.assert_array_equal(sorted_hs, expected_hs) @pytest.mark.parametrize( @@ -159,6 +164,8 @@ def test_gray_code_comparison(seq_a, seq_b, expected): # However, the in-between has to share the same target/control. ([(0, 1), (0, 2), (0, 1)], False, False, [(0, 1), (0, 2), (0, 1)]), ([(0, 1), (2, 1), (0, 1)], True, False, [(0, 1), (2, 1), (0, 1)]), + # Can simplify, but violates CNOT ordering assumption + ([(0, 1), (2, 3), (0, 1)], False, False, [(0, 1), (2, 3), (0, 1)]), ], ) def test_simplify_commuting_cnots( @@ -183,6 +190,9 @@ def test_simplify_commuting_cnots( # Simplify according to equation 11 of [4]. ([(2, 1), (2, 0), (1, 0)], False, True, [(1, 0), (2, 1)]), ([(1, 2), (0, 2), (0, 1)], True, True, [(0, 1), (1, 2)]), + # Same as above, but with a intervening CNOTs that prevent simplifications. + ([(2, 1), (2, 0), (100, 101), (1, 0)], False, False, [(2, 1), (2, 0), (100, 101), (1, 0)]), + ([(2, 1), (100, 101), (2, 0), (1, 0)], False, False, [(2, 1), (100, 101), (2, 0), (1, 0)]), ], ) def test_simplify_cnots_triplets( From 1da73495e8b9fca7ee5e835564eaedc3958a12ce Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 25 Sep 2021 13:31:43 +0000 Subject: [PATCH 03/10] Address some of the comments --- cirq-core/cirq/ops/boolean_hamiltonian.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index afa384b1307..03e5c114353 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -172,8 +172,8 @@ def _simplify_commuting_cnots( flip_control_and_target: Whether to flip control and target. Returns: - A Boolean that tells whether a simplification has been performed. - The CNOT list, potentially simplified. + A tuple containing a Boolean that tells whether a simplification has been performed and the + CNOT list, potentially simplified, encoded as integer tuples (control, target). """ target, control = (0, 1) if flip_control_and_target else (1, 0) @@ -191,6 +191,7 @@ def _simplify_commuting_cnots( k = qubit_to_index[cnots[j][control]] # The controls (resp. targets) are the same, so we can simplify away. cnots = [cnots[n] for n in range(len(cnots)) if n != j and n != k] + # TODO(#4532): Speed up code by not returning early. return True, cnots qubit_to_index[cnots[j][control]] = j @@ -215,8 +216,8 @@ def _simplify_cnots_triplets( flip_control_and_target: Whether to flip control and target. Returns: - A Boolean that tells whether a simplification has been performed. - The CNOT list, potentially simplified, encoded as integer tuples (control, target). + A tuple containing a Boolean that tells whether a simplification has been performed and the + CNOT list, potentially simplified, encoded as integer tuples (control, target). """ target, control = (0, 1) if flip_control_and_target else (1, 0) @@ -251,6 +252,7 @@ def _simplify_cnots_triplets( ) # Since we removed the pivot, the length should be one fewer. cnots = [cnots[idx] for idx in new_idx] + # TODO(#4532): Speed up code by not returning early. return True, cnots return False, cnots From 313e37e3238594e3e2cbdb60f2173853e7eefb8d Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Tue, 5 Oct 2021 03:44:51 +0000 Subject: [PATCH 04/10] Add unit test --- cirq-core/cirq/ops/boolean_hamiltonian_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index b59e125907e..9fd611de7df 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -193,6 +193,9 @@ def test_simplify_commuting_cnots( # Same as above, but with a intervening CNOTs that prevent simplifications. ([(2, 1), (2, 0), (100, 101), (1, 0)], False, False, [(2, 1), (2, 0), (100, 101), (1, 0)]), ([(2, 1), (100, 101), (2, 0), (1, 0)], False, False, [(2, 1), (100, 101), (2, 0), (1, 0)]), + # swap (2, 1) and (1, 0) around (2, 0) + ([(2, 1), (2, 3), (2, 0), (3, 0), (1, 0)], False, True, [(2, 3), (1, 0), (2, 1), (3, 0)]) + ], ) def test_simplify_cnots_triplets( From 458018afbd0b79b2e7b1610e89d805816dea3256 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Tue, 5 Oct 2021 04:27:40 +0000 Subject: [PATCH 05/10] Expand unit test --- .../cirq/ops/boolean_hamiltonian_test.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index 9fd611de7df..900d03dbea6 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -194,8 +194,7 @@ def test_simplify_commuting_cnots( ([(2, 1), (2, 0), (100, 101), (1, 0)], False, False, [(2, 1), (2, 0), (100, 101), (1, 0)]), ([(2, 1), (100, 101), (2, 0), (1, 0)], False, False, [(2, 1), (100, 101), (2, 0), (1, 0)]), # swap (2, 1) and (1, 0) around (2, 0) - ([(2, 1), (2, 3), (2, 0), (3, 0), (1, 0)], False, True, [(2, 3), (1, 0), (2, 1), (3, 0)]) - + ([(2, 1), (2, 3), (2, 0), (3, 0), (1, 0)], False, True, [(2, 3), (1, 0), (2, 1), (3, 0)]), ], ) def test_simplify_cnots_triplets( @@ -206,3 +205,22 @@ def test_simplify_cnots_triplets( ) assert actual_simplified == expected_simplified assert actual_output_cnots == expected_output_cnots + + # Check that the unitaries are the same. + num_qubits = max(max(x) for x in input_cnots) + 1 + qubits = cirq.LineQubit.range(num_qubits) + + target, control = (0, 1) if input_flip_control_and_target else (1, 0) + + circuit_input = cirq.Circuit() + for input_cnot in input_cnots: + circuit_input.append(cirq.CNOT(qubits[input_cnot[target]], qubits[input_cnot[control]])) + circuit_expected = cirq.Circuit() + for expected_output_cnot in expected_output_cnots: + circuit_expected.append( + cirq.CNOT(qubits[expected_output_cnot[target]], qubits[expected_output_cnot[control]]) + ) + + np.testing.assert_allclose( + cirq.unitary(circuit_input), cirq.unitary(circuit_expected), atol=1e-6 + ) From 0b3db3cdc7040b169585843c58139daea69c071c Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Tue, 5 Oct 2021 04:34:04 +0000 Subject: [PATCH 06/10] Fix unit test --- cirq-core/cirq/ops/boolean_hamiltonian_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index 900d03dbea6..45e19c49e27 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -207,8 +207,8 @@ def test_simplify_cnots_triplets( assert actual_output_cnots == expected_output_cnots # Check that the unitaries are the same. - num_qubits = max(max(x) for x in input_cnots) + 1 - qubits = cirq.LineQubit.range(num_qubits) + qubit_ids = set(sum(input_cnots, ())) + qubits = {qubit_id: cirq.NamedQubit(f"{qubit_id}") for qubit_id in qubit_ids} target, control = (0, 1) if input_flip_control_and_target else (1, 0) From d098d18ab5cd65c0d4c14d0d46c7e014bc0a07b0 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Tue, 5 Oct 2021 04:46:07 +0000 Subject: [PATCH 07/10] Add test that is failing but should pass --- cirq-core/cirq/ops/boolean_hamiltonian_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index 45e19c49e27..7cfac9162a8 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -194,6 +194,7 @@ def test_simplify_commuting_cnots( ([(2, 1), (2, 0), (100, 101), (1, 0)], False, False, [(2, 1), (2, 0), (100, 101), (1, 0)]), ([(2, 1), (100, 101), (2, 0), (1, 0)], False, False, [(2, 1), (100, 101), (2, 0), (1, 0)]), # swap (2, 1) and (1, 0) around (2, 0) + ([(2, 1), (2, 3), (2, 0), (3, 0), (1, 0)], True, True, [(2, 3), (1, 0), (2, 1), (3, 0)]), ([(2, 1), (2, 3), (2, 0), (3, 0), (1, 0)], False, True, [(2, 3), (1, 0), (2, 1), (3, 0)]), ], ) From 6651ea6c002d8c8feea554e43488e25478485d86 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Wed, 6 Oct 2021 04:22:07 +0000 Subject: [PATCH 08/10] More comprehensive tests --- .../cirq/ops/boolean_hamiltonian_test.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index 7cfac9162a8..9276e865914 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -194,8 +194,11 @@ def test_simplify_commuting_cnots( ([(2, 1), (2, 0), (100, 101), (1, 0)], False, False, [(2, 1), (2, 0), (100, 101), (1, 0)]), ([(2, 1), (100, 101), (2, 0), (1, 0)], False, False, [(2, 1), (100, 101), (2, 0), (1, 0)]), # swap (2, 1) and (1, 0) around (2, 0) - ([(2, 1), (2, 3), (2, 0), (3, 0), (1, 0)], True, True, [(2, 3), (1, 0), (2, 1), (3, 0)]), ([(2, 1), (2, 3), (2, 0), (3, 0), (1, 0)], False, True, [(2, 3), (1, 0), (2, 1), (3, 0)]), + ([(2, 1), (2, 0), (2, 3), (3, 0), (1, 0)], False, True, [(2, 3), (1, 0), (2, 1), (3, 0)]), + ([(2, 3), (2, 1), (2, 0), (3, 0), (1, 0)], False, True, [(2, 3), (1, 0), (2, 1), (3, 0)]), + ([(2, 1), (2, 3), (3, 0), (2, 0), (1, 0)], False, True, [(2, 3), (1, 0), (2, 1), (3, 0)]), + ([(2, 1), (2, 3), (2, 0), (1, 0), (3, 0)], False, True, [(2, 3), (1, 0), (2, 1), (3, 0)]), ], ) def test_simplify_cnots_triplets( @@ -216,12 +219,8 @@ def test_simplify_cnots_triplets( circuit_input = cirq.Circuit() for input_cnot in input_cnots: circuit_input.append(cirq.CNOT(qubits[input_cnot[target]], qubits[input_cnot[control]])) - circuit_expected = cirq.Circuit() - for expected_output_cnot in expected_output_cnots: - circuit_expected.append( - cirq.CNOT(qubits[expected_output_cnot[target]], qubits[expected_output_cnot[control]]) - ) - - np.testing.assert_allclose( - cirq.unitary(circuit_input), cirq.unitary(circuit_expected), atol=1e-6 - ) + circuit_actual = cirq.Circuit() + for actual_cnot in actual_output_cnots: + circuit_actual.append(cirq.CNOT(qubits[actual_cnot[target]], qubits[actual_cnot[control]])) + + np.testing.assert_allclose(cirq.unitary(circuit_input), cirq.unitary(circuit_actual), atol=1e-6) From e82714a34e211a82760794863ca6b01d45417ca6 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 8 Oct 2021 23:30:56 +0000 Subject: [PATCH 09/10] Fix code and make unit tests pass --- cirq-core/cirq/ops/boolean_hamiltonian.py | 13 +++++++++++++ cirq-core/cirq/ops/boolean_hamiltonian_test.py | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 03e5c114353..87a19e89b63 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -225,8 +225,13 @@ def _simplify_cnots_triplets( for j in range(1, len(cnots) - 1): # First, we look back for as long as the controls (resp. targets) are the same. # They all commute, so all are potential candidates for being simplified. + # prev_match_index is qubit to index in `cnots` array. prev_match_index: Dict[int, int] = {} for i in range(j - 1, -1, -1): + # These CNOTs have the same target (resp. control) and though they are not candidates + # for simplification, since they commute, we can keep looking for candidates. + if cnots[i][target] == cnots[j][target]: + continue if cnots[i][control] != cnots[j][control]: break # We take a note of the control (resp. target). @@ -234,8 +239,13 @@ def _simplify_cnots_triplets( # Next, we look forward for as long as the targets (resp. controls) are the # same. They all commute, so all are potential candidates for being simplified. + # post_match_index is qubit to index in `cnots` array. post_match_index: Dict[int, int] = {} for k in range(j + 1, len(cnots)): + # These CNOTs have the same control (resp. target ) and though they are not candidates + # for simplification, since they commute, we can keep looking for candidates. + if cnots[j][control] == cnots[k][control]: + continue if cnots[j][target] != cnots[k][target]: break # We take a note of the target (resp. control). @@ -246,8 +256,11 @@ def _simplify_cnots_triplets( for key in keys: # We perform the swap which removes the pivot. new_idx: List[int] = ( + # Anything strictly before the pivot that is not the CNOT to swap. [idx for idx in range(0, j) if idx != prev_match_index[key]] + # The two swapped CNOTs. + [post_match_index[key], prev_match_index[key]] + # Anything after the pivot that is not the CNOT to swap. + [idx for idx in range(j + 1, len(cnots)) if idx != post_match_index[key]] ) # Since we removed the pivot, the length should be one fewer. diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index 9276e865914..fc6f6146f0a 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -188,16 +188,16 @@ def test_simplify_commuting_cnots( ([(0, 1)], False, False, [(0, 1)]), ([(0, 1)], True, False, [(0, 1)]), # Simplify according to equation 11 of [4]. - ([(2, 1), (2, 0), (1, 0)], False, True, [(1, 0), (2, 1)]), + ([(2, 1), (2, 0), (1, 0)], False, True, [(1, 0), (2, 1)]), # Template without interweening ([(1, 2), (0, 2), (0, 1)], True, True, [(0, 1), (1, 2)]), # Same as above, but with a intervening CNOTs that prevent simplifications. ([(2, 1), (2, 0), (100, 101), (1, 0)], False, False, [(2, 1), (2, 0), (100, 101), (1, 0)]), ([(2, 1), (100, 101), (2, 0), (1, 0)], False, False, [(2, 1), (100, 101), (2, 0), (1, 0)]), # swap (2, 1) and (1, 0) around (2, 0) ([(2, 1), (2, 3), (2, 0), (3, 0), (1, 0)], False, True, [(2, 3), (1, 0), (2, 1), (3, 0)]), - ([(2, 1), (2, 0), (2, 3), (3, 0), (1, 0)], False, True, [(2, 3), (1, 0), (2, 1), (3, 0)]), + ([(2, 1), (2, 0), (2, 3), (3, 0), (1, 0)], False, True, [(1, 0), (2, 1), (2, 3), (3, 0)]), ([(2, 3), (2, 1), (2, 0), (3, 0), (1, 0)], False, True, [(2, 3), (1, 0), (2, 1), (3, 0)]), - ([(2, 1), (2, 3), (3, 0), (2, 0), (1, 0)], False, True, [(2, 3), (1, 0), (2, 1), (3, 0)]), + ([(2, 1), (2, 3), (3, 0), (2, 0), (1, 0)], False, True, [(2, 3), (3, 0), (1, 0), (2, 1)]), ([(2, 1), (2, 3), (2, 0), (1, 0), (3, 0)], False, True, [(2, 3), (1, 0), (2, 1), (3, 0)]), ], ) From 697d90441c3f81d1a97fa3255210d821b249096e Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Mon, 11 Oct 2021 23:42:13 +0000 Subject: [PATCH 10/10] Address comments --- cirq-core/cirq/ops/boolean_hamiltonian.py | 2 +- cirq-core/cirq/ops/boolean_hamiltonian_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 87a19e89b63..0be4a2a7241 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -242,7 +242,7 @@ def _simplify_cnots_triplets( # post_match_index is qubit to index in `cnots` array. post_match_index: Dict[int, int] = {} for k in range(j + 1, len(cnots)): - # These CNOTs have the same control (resp. target ) and though they are not candidates + # These CNOTs have the same control (resp. target) and though they are not candidates # for simplification, since they commute, we can keep looking for candidates. if cnots[j][control] == cnots[k][control]: continue diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index fc6f6146f0a..9671101210e 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -188,7 +188,7 @@ def test_simplify_commuting_cnots( ([(0, 1)], False, False, [(0, 1)]), ([(0, 1)], True, False, [(0, 1)]), # Simplify according to equation 11 of [4]. - ([(2, 1), (2, 0), (1, 0)], False, True, [(1, 0), (2, 1)]), # Template without interweening + ([(2, 1), (2, 0), (1, 0)], False, True, [(1, 0), (2, 1)]), ([(1, 2), (0, 2), (0, 1)], True, True, [(0, 1), (1, 2)]), # Same as above, but with a intervening CNOTs that prevent simplifications. ([(2, 1), (2, 0), (100, 101), (1, 0)], False, False, [(2, 1), (2, 0), (100, 101), (1, 0)]),