Skip to content
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

Boolean Hamiltonian gate yields fewer gates #4386

Merged
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0ae665c
Boolean Hamiltonian gate yields fewer gates
tonybruguier Aug 6, 2021
fda9857
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Aug 8, 2021
996bf8d
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Aug 9, 2021
452fbd7
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Aug 10, 2021
61b2509
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Aug 11, 2021
662b446
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Aug 12, 2021
2016c60
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Aug 14, 2021
a67c549
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Aug 17, 2021
c2fe108
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Aug 18, 2021
61a078e
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Aug 19, 2021
fcbeb45
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Aug 19, 2021
b4063a7
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Aug 21, 2021
e383422
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Aug 26, 2021
2756525
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Aug 29, 2021
fe4ddfd
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Sep 2, 2021
6388c23
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Sep 8, 2021
8c992ed
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Sep 16, 2021
35ed9be
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Sep 17, 2021
61ea645
Address some of the comments
tonybruguier Sep 18, 2021
526572a
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Sep 25, 2021
1da7349
Address some of the comments
tonybruguier Sep 25, 2021
4244551
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Sep 29, 2021
fce3b01
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Oct 2, 2021
b62d031
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Oct 4, 2021
83f79c4
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Oct 5, 2021
313e37e
Add unit test
tonybruguier Oct 5, 2021
458018a
Expand unit test
tonybruguier Oct 5, 2021
0b3db3c
Fix unit test
tonybruguier Oct 5, 2021
d098d18
Add test that is failing but should pass
tonybruguier Oct 5, 2021
880235b
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Oct 6, 2021
6651ea6
More comprehensive tests
tonybruguier Oct 6, 2021
18b55ec
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Oct 8, 2021
9087b97
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Oct 8, 2021
e82714a
Fix code and make unit tests pass
tonybruguier Oct 8, 2021
697d904
Address comments
tonybruguier Oct 11, 2021
3e8b5f6
Merge branch 'master' of github.com:quantumlib/Cirq into simplify_boo…
tonybruguier Oct 11, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 177 additions & 5 deletions cirq-core/cirq/ops/boolean_hamiltonian.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
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

from typing import Any, Dict, Generator, List, Sequence, Tuple

Expand Down Expand Up @@ -112,6 +116,172 @@ 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]:
tonybruguier marked this conversation as resolved.
Show resolved Hide resolved
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). 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:
A Boolean that tells whether a simplification has been performed.
The CNOT list, potentially simplified.
tonybruguier marked this conversation as resolved.
Show resolved Hide resolved
"""

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
tonybruguier marked this conversation as resolved.
Show resolved Hide resolved

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, 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 controls (resp. targets) are the same.
# They all commute, so all are potential candidates for being simplified.
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).
prev_match_index[cnots[i][target]] = i

# 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: 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).
post_match_index[cnots[k][control]] = k

# Among all the candidates, find if they have a match.
keys = prev_match_index.keys() & post_match_index.keys()
for key in keys:
# We perform the swap which removes the pivot.
new_idx: List[int] = (
[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.
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:
The simplified list of CNOTs, encoded as integer tuples (control, target).
"""

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'],
Expand Down Expand Up @@ -145,16 +315,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:
Expand Down
105 changes: 105 additions & 0 deletions cirq-core/cirq/ops/boolean_hamiltonian_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -98,3 +101,105 @@ def test_with_custom_names():

with pytest.raises(ValueError, match='Length of replacement qubits must be the same'):
original_op.with_qubits(q2)


@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_template = []
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_template.append(tuple(sorted(h)))

for seed in range(10):
random.seed(seed)

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(
'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.
tonybruguier marked this conversation as resolved.
Show resolved Hide resolved
([(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(
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)]),
tonybruguier marked this conversation as resolved.
Show resolved Hide resolved
# 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(
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