From 914f3d52899bf7278829512c3a54b7abf094783e Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 3 Apr 2021 06:54:34 +0000 Subject: [PATCH 01/87] Hamiltonian representation of binary functions --- examples/hamiltonian_representation.py | 139 ++++++++++++++++++++ examples/hamiltonian_representation_test.py | 58 ++++++++ 2 files changed, 197 insertions(+) create mode 100644 examples/hamiltonian_representation.py create mode 100644 examples/hamiltonian_representation_test.py diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py new file mode 100644 index 00000000000..73f9b76e18d --- /dev/null +++ b/examples/hamiltonian_representation.py @@ -0,0 +1,139 @@ +import math +from typing import Dict, Tuple + +from sympy.logic.boolalg import And, Not, Or, Xor +from sympy.core.symbol import Symbol + +import cirq + +# References: +# [1] On the representation of Boolean and real functions as Hamil tonians for quantum computing +# by StuartHadfield +# [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 + +# TODO(tonybruguier): Hook up this code with the QAOA example so that we can solve generic problems +# instead of just the max-cut example. + + +class HamiltonianList: + """A container class of Boolean function as equation (2) or [1]""" + + def __init__(self, hamiltonians: Dict[Tuple[int, ...], float]): + self.hamiltonians = {h: w for h, w in hamiltonians.items() if math.fabs(w) > 1e-12} + + def __str__(self): + # For run-to-run identicalness, we sort the keys lexicographically. + return "; ".join( + "%.2f.%s" % (self.hamiltonians[h], ".".join("Z_%d" % (d) for d in h) if h else 'I') + for h in sorted(self.hamiltonians) + ) + + def __add__(self, other): + return self._signed_add(other, 1.0) + + def __sub__(self, other): + return self._signed_add(other, -1.0) + + def _signed_add(self, other, sign: float): + hamiltonians = self.hamiltonians.copy() + for h, w in other.hamiltonians.items(): + if h not in hamiltonians: + hamiltonians[h] = sign * w + else: + hamiltonians[h] += sign * w + return HamiltonianList(hamiltonians) + + def __rmul__(self, other: float): + return HamiltonianList({k: other * w for k, w in self.hamiltonians.items()}) + + def __mul__(self, other): + hamiltonians = {} + for h1, w1 in self.hamiltonians.items(): + for h2, w2 in other.hamiltonians.items(): + h = tuple(set(h1).symmetric_difference(h2)) + w = w1 * w2 + if h not in hamiltonians: + hamiltonians[h] = w + else: + hamiltonians[h] += w + return HamiltonianList(hamiltonians) + + @staticmethod + def O(): + return HamiltonianList({}) + + @staticmethod + def I(): + return HamiltonianList({(): 1.0}) + + @staticmethod + def Z(i: int): + return HamiltonianList({(i,): 1.0}) + + +def build_hamiltonian_from_boolean(boolean_expr, name_to_id) -> HamiltonianList: + """Builds the Hamiltonian representation of Boolean expression as per [1]: + + Args: + boolean_expr: A Sympy expression containing symbols and Boolean operations + name_to_id: A dictionary from symbol name to an integer, typically built by calling + get_name_to_id(). + + Return: + The HamiltonianList that represents the Boolean expression. + """ + + if isinstance(boolean_expr, (And, Not, Or, Xor)): + sub_hamiltonians = [ + build_hamiltonian_from_boolean(sub_boolean_expr, name_to_id) + for sub_boolean_expr in boolean_expr.args + ] + # We apply the equalities of theorem 1 of [1]. + if isinstance(boolean_expr, And): + hamiltonian = HamiltonianList.I() + for sub_hamiltonian in sub_hamiltonians: + hamiltonian = hamiltonian * sub_hamiltonian + elif isinstance(boolean_expr, Not): + assert len(sub_hamiltonians) == 1 + hamiltonian = HamiltonianList.I() - sub_hamiltonians[0] + elif isinstance(boolean_expr, Or): + hamiltonian = HamiltonianList.O() + for sub_hamiltonian in sub_hamiltonians: + hamiltonian = hamiltonian + sub_hamiltonian - hamiltonian * sub_hamiltonian + elif isinstance(boolean_expr, Xor): + hamiltonian = HamiltonianList.O() + for sub_hamiltonian in sub_hamiltonians: + hamiltonian = hamiltonian + sub_hamiltonian - 2.0 * hamiltonian * sub_hamiltonian + return hamiltonian + elif isinstance(boolean_expr, Symbol): + # Table 1 of [1], entry for 'x' is '1/2.I - 1/2.Z' + i = name_to_id[boolean_expr.name] + return 0.5 * HamiltonianList.I() - 0.5 * HamiltonianList.Z(i) + else: + raise ValueError(f'Unsupported type: {type(boolean_expr)}') + + +def get_name_to_id(boolean_expr): + # For run-to-run identicalness, we sort the symbol name lexicographically. + symbol_names = sorted(symbol.name for symbol in boolean_expr.free_symbols) + return {symbol_name: i for i, symbol_name in enumerate(symbol_names)} + + +def build_circuit_from_hamiltonian(hamiltonian, name_to_id, theta): + qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] + circuit = cirq.Circuit() + + circuit.append(cirq.H.on_each(*qubits)) + + for h, w in hamiltonian.hamiltonians.items(): + for i in range(1, len(h)): + circuit.append(cirq.CNOT(qubits[h[i]], qubits[h[0]])) + + if len(h) >= 1: + circuit.append(cirq.Rz(rads=(theta * w)).on(qubits[h[0]])) + + for i in range(1, len(h)): + circuit.append(cirq.CNOT(qubits[h[i]], qubits[h[0]])) + + return circuit, qubits diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py new file mode 100644 index 00000000000..a0a1d4f0af7 --- /dev/null +++ b/examples/hamiltonian_representation_test.py @@ -0,0 +1,58 @@ +import math + +import numpy as np +import pytest +from sympy.parsing.sympy_parser import parse_expr + +import cirq +import examples.hamiltonian_representation as hr + +# These are some of the entries of table 1. +@pytest.mark.parametrize( + 'boolean_expr,hamiltonian', + [ + ('x', '0.50.I; -0.50.Z_0'), + ('~x', '0.50.I; 0.50.Z_0'), + ('x0 ^ x1', '0.50.I; -0.50.Z_0.Z_1'), + ('x0 & x1', '0.25.I; -0.25.Z_0; 0.25.Z_0.Z_1; -0.25.Z_1'), + ('x0 | x1', '0.75.I; -0.25.Z_0; -0.25.Z_0.Z_1; -0.25.Z_1'), + ('x0 ^ x1 ^ x2', '0.50.I; -0.50.Z_0.Z_1.Z_2'), + ], +) +def test_build_hamiltonian_from_boolean(boolean_expr, hamiltonian): + boolean = parse_expr(boolean_expr) + name_to_id = hr.get_name_to_id(boolean) + actual = hr.build_hamiltonian_from_boolean(boolean, name_to_id) + assert hamiltonian == str(actual) + + +def test_unsupported_op(): + not_a_boolean = parse_expr('x * x') + name_to_id = hr.get_name_to_id(not_a_boolean) + with pytest.raises(ValueError, match='Unsupported type'): + hr.build_hamiltonian_from_boolean(not_a_boolean, name_to_id) + + +@pytest.mark.parametrize( + 'boolean_expr, expected', + [ + ('x', [False, True]), + ('~x', [True, False]), + ('x0 ^ x1', [False, True, True, False]), + ('x0 & x1', [False, False, False, True]), + ('x0 | x1', [False, True, True, True]), + ('x0 & ~x1 & x2', [False, False, False, False, False, True, False, False]), + ], +) +def test_circuit(boolean_expr, expected): + boolean = parse_expr(boolean_expr) + name_to_id = hr.get_name_to_id(boolean) + hamiltonian = hr.build_hamiltonian_from_boolean(boolean, name_to_id) + + theta = 0.1 * math.pi + circuit, qubits = hr.build_circuit_from_hamiltonian(hamiltonian, name_to_id, theta) + + phi = cirq.Simulator().simulate(circuit, qubit_order=qubits, initial_state=0).state_vector() + actual = np.arctan2(phi.real, phi.imag) - math.pi / 2.0 > 0.0 + + np.testing.assert_array_equal(actual, expected) From a0c19efd899bde0d37b9c3672d013f01b217cc05 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 3 Apr 2021 19:06:02 +0000 Subject: [PATCH 02/87] Allow for multiple expressions --- examples/hamiltonian_representation.py | 23 ++++++++++++--------- examples/hamiltonian_representation_test.py | 8 +++---- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 73f9b76e18d..962511a6d47 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -114,26 +114,29 @@ def build_hamiltonian_from_boolean(boolean_expr, name_to_id) -> HamiltonianList: raise ValueError(f'Unsupported type: {type(boolean_expr)}') -def get_name_to_id(boolean_expr): +def get_name_to_id(boolean_exprs): # For run-to-run identicalness, we sort the symbol name lexicographically. - symbol_names = sorted(symbol.name for symbol in boolean_expr.free_symbols) + symbol_names = sorted( + symbol.name for boolean_expr in boolean_exprs for symbol in boolean_expr.free_symbols + ) return {symbol_name: i for i, symbol_name in enumerate(symbol_names)} -def build_circuit_from_hamiltonian(hamiltonian, name_to_id, theta): +def build_circuit_from_hamiltonians(hamiltonians, name_to_id, theta): qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] circuit = cirq.Circuit() circuit.append(cirq.H.on_each(*qubits)) - for h, w in hamiltonian.hamiltonians.items(): - for i in range(1, len(h)): - circuit.append(cirq.CNOT(qubits[h[i]], qubits[h[0]])) + for hamiltonian in hamiltonians: + for h, w in hamiltonian.hamiltonians.items(): + for i in range(1, len(h)): + circuit.append(cirq.CNOT(qubits[h[i]], qubits[h[0]])) - if len(h) >= 1: - circuit.append(cirq.Rz(rads=(theta * w)).on(qubits[h[0]])) + if len(h) >= 1: + circuit.append(cirq.Rz(rads=(theta * w)).on(qubits[h[0]])) - for i in range(1, len(h)): - circuit.append(cirq.CNOT(qubits[h[i]], qubits[h[0]])) + for i in range(1, len(h)): + circuit.append(cirq.CNOT(qubits[h[i]], qubits[h[0]])) return circuit, qubits diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index a0a1d4f0af7..701c55ef025 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -21,14 +21,14 @@ ) def test_build_hamiltonian_from_boolean(boolean_expr, hamiltonian): boolean = parse_expr(boolean_expr) - name_to_id = hr.get_name_to_id(boolean) + name_to_id = hr.get_name_to_id([boolean]) actual = hr.build_hamiltonian_from_boolean(boolean, name_to_id) assert hamiltonian == str(actual) def test_unsupported_op(): not_a_boolean = parse_expr('x * x') - name_to_id = hr.get_name_to_id(not_a_boolean) + name_to_id = hr.get_name_to_id([not_a_boolean]) with pytest.raises(ValueError, match='Unsupported type'): hr.build_hamiltonian_from_boolean(not_a_boolean, name_to_id) @@ -46,11 +46,11 @@ def test_unsupported_op(): ) def test_circuit(boolean_expr, expected): boolean = parse_expr(boolean_expr) - name_to_id = hr.get_name_to_id(boolean) + name_to_id = hr.get_name_to_id([boolean]) hamiltonian = hr.build_hamiltonian_from_boolean(boolean, name_to_id) theta = 0.1 * math.pi - circuit, qubits = hr.build_circuit_from_hamiltonian(hamiltonian, name_to_id, theta) + circuit, qubits = hr.build_circuit_from_hamiltonians([hamiltonian], name_to_id, theta) phi = cirq.Simulator().simulate(circuit, qubit_order=qubits, initial_state=0).state_vector() actual = np.arctan2(phi.real, phi.imag) - math.pi / 2.0 > 0.0 From 91927924c1f51b3fa76cbda547fa523c59144d5a Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 3 Apr 2021 19:21:29 +0000 Subject: [PATCH 03/87] De-dupe names --- examples/hamiltonian_representation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 962511a6d47..e4f334555f7 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -116,9 +116,9 @@ def build_hamiltonian_from_boolean(boolean_expr, name_to_id) -> HamiltonianList: def get_name_to_id(boolean_exprs): # For run-to-run identicalness, we sort the symbol name lexicographically. - symbol_names = sorted( + symbol_names = sorted({ symbol.name for boolean_expr in boolean_exprs for symbol in boolean_expr.free_symbols - ) + }) return {symbol_name: i for i, symbol_name in enumerate(symbol_names)} From 739734bf62e711da9fb7ba5aafbd35e329fcf6c8 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 3 Apr 2021 19:27:55 +0000 Subject: [PATCH 04/87] Add to existing circuit --- examples/hamiltonian_representation.py | 14 +++++--------- examples/hamiltonian_representation_test.py | 6 +++++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index e4f334555f7..642fe85cab6 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -116,18 +116,14 @@ def build_hamiltonian_from_boolean(boolean_expr, name_to_id) -> HamiltonianList: def get_name_to_id(boolean_exprs): # For run-to-run identicalness, we sort the symbol name lexicographically. - symbol_names = sorted({ - symbol.name for boolean_expr in boolean_exprs for symbol in boolean_expr.free_symbols - }) + symbol_names = sorted( + {symbol.name for boolean_expr in boolean_exprs for symbol in boolean_expr.free_symbols} + ) return {symbol_name: i for i, symbol_name in enumerate(symbol_names)} -def build_circuit_from_hamiltonians(hamiltonians, name_to_id, theta): - qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] +def build_circuit_from_hamiltonians(hamiltonians, qubits, theta): circuit = cirq.Circuit() - - circuit.append(cirq.H.on_each(*qubits)) - for hamiltonian in hamiltonians: for h, w in hamiltonian.hamiltonians.items(): for i in range(1, len(h)): @@ -139,4 +135,4 @@ def build_circuit_from_hamiltonians(hamiltonians, name_to_id, theta): for i in range(1, len(h)): circuit.append(cirq.CNOT(qubits[h[i]], qubits[h[0]])) - return circuit, qubits + return circuit diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index 701c55ef025..62ad79817d0 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -49,8 +49,12 @@ def test_circuit(boolean_expr, expected): name_to_id = hr.get_name_to_id([boolean]) hamiltonian = hr.build_hamiltonian_from_boolean(boolean, name_to_id) + qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] + circuit = cirq.Circuit() + circuit.append(cirq.H.on_each(*qubits)) + theta = 0.1 * math.pi - circuit, qubits = hr.build_circuit_from_hamiltonians([hamiltonian], name_to_id, theta) + circuit += hr.build_circuit_from_hamiltonians([hamiltonian], qubits, theta) phi = cirq.Simulator().simulate(circuit, qubit_order=qubits, initial_state=0).state_vector() actual = np.arctan2(phi.real, phi.imag) - math.pi / 2.0 > 0.0 From 579dc3332f8030e20f53705b7a91b04206e85b50 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 3 Apr 2021 19:27:55 +0000 Subject: [PATCH 05/87] Add to existing circuit --- examples/hamiltonian_representation.py | 14 +++++--------- examples/hamiltonian_representation_test.py | 6 +++++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index e4f334555f7..642fe85cab6 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -116,18 +116,14 @@ def build_hamiltonian_from_boolean(boolean_expr, name_to_id) -> HamiltonianList: def get_name_to_id(boolean_exprs): # For run-to-run identicalness, we sort the symbol name lexicographically. - symbol_names = sorted({ - symbol.name for boolean_expr in boolean_exprs for symbol in boolean_expr.free_symbols - }) + symbol_names = sorted( + {symbol.name for boolean_expr in boolean_exprs for symbol in boolean_expr.free_symbols} + ) return {symbol_name: i for i, symbol_name in enumerate(symbol_names)} -def build_circuit_from_hamiltonians(hamiltonians, name_to_id, theta): - qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] +def build_circuit_from_hamiltonians(hamiltonians, qubits, theta): circuit = cirq.Circuit() - - circuit.append(cirq.H.on_each(*qubits)) - for hamiltonian in hamiltonians: for h, w in hamiltonian.hamiltonians.items(): for i in range(1, len(h)): @@ -139,4 +135,4 @@ def build_circuit_from_hamiltonians(hamiltonians, name_to_id, theta): for i in range(1, len(h)): circuit.append(cirq.CNOT(qubits[h[i]], qubits[h[0]])) - return circuit, qubits + return circuit diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index 701c55ef025..62ad79817d0 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -49,8 +49,12 @@ def test_circuit(boolean_expr, expected): name_to_id = hr.get_name_to_id([boolean]) hamiltonian = hr.build_hamiltonian_from_boolean(boolean, name_to_id) + qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] + circuit = cirq.Circuit() + circuit.append(cirq.H.on_each(*qubits)) + theta = 0.1 * math.pi - circuit, qubits = hr.build_circuit_from_hamiltonians([hamiltonian], name_to_id, theta) + circuit += hr.build_circuit_from_hamiltonians([hamiltonian], qubits, theta) phi = cirq.Simulator().simulate(circuit, qubit_order=qubits, initial_state=0).state_vector() actual = np.arctan2(phi.real, phi.imag) - math.pi / 2.0 > 0.0 From 78d3969044d0a026f1a3db97c298a7f67ab17109 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sun, 4 Apr 2021 03:41:12 +0000 Subject: [PATCH 06/87] fix coverage and formatting --- examples/hamiltonian_representation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 642fe85cab6..003000bb9b4 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -25,7 +25,7 @@ def __init__(self, hamiltonians: Dict[Tuple[int, ...], float]): def __str__(self): # For run-to-run identicalness, we sort the keys lexicographically. return "; ".join( - "%.2f.%s" % (self.hamiltonians[h], ".".join("Z_%d" % (d) for d in h) if h else 'I') + f"{self.hamiltonians[h]:.2f}.{'.'.join('Z_%d' % d for d in h) if h else 'I'}" for h in sorted(self.hamiltonians) ) @@ -56,6 +56,7 @@ def __mul__(self, other): if h not in hamiltonians: hamiltonians[h] = w else: + breakpoint() hamiltonians[h] += w return HamiltonianList(hamiltonians) From 499083e4ed52857804bbfe7bd5b5a6766b5bdb18 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sun, 4 Apr 2021 04:01:50 +0000 Subject: [PATCH 07/87] add missing file --- examples/hamiltonian_representation.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 003000bb9b4..97bb429ac58 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -39,9 +39,8 @@ def _signed_add(self, other, sign: float): hamiltonians = self.hamiltonians.copy() for h, w in other.hamiltonians.items(): if h not in hamiltonians: - hamiltonians[h] = sign * w - else: - hamiltonians[h] += sign * w + hamiltonians[h] = 0 + hamiltonians[h] += sign * w return HamiltonianList(hamiltonians) def __rmul__(self, other: float): @@ -54,10 +53,8 @@ def __mul__(self, other): h = tuple(set(h1).symmetric_difference(h2)) w = w1 * w2 if h not in hamiltonians: - hamiltonians[h] = w - else: - breakpoint() - hamiltonians[h] += w + hamiltonians[h] = 0 + hamiltonians[h] += w return HamiltonianList(hamiltonians) @staticmethod From 5251a786017ede9839867c4efb6b9c86655f1223 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sun, 4 Apr 2021 20:20:01 +0000 Subject: [PATCH 08/87] Modify QAOA --- examples/qaoa.py | 168 ++++++++++++----------------------------------- 1 file changed, 42 insertions(+), 126 deletions(-) diff --git a/examples/qaoa.py b/examples/qaoa.py index 87528dc25ac..2af26aa436f 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -1,155 +1,71 @@ -"""Runs the Quantum Approximate Optimization Algorithm on Max-Cut. - -=== EXAMPLE OUTPUT === - -Example QAOA circuit: - 0 1 2 3 4 5 - │ │ │ │ │ │ - H H H H H H - │ │ │ │ │ │ - ZZ──────────ZZ^(-4/13) │ │ │ │ -┌ │ │ │ │ │ │ ┐ -│ ZZ──────────┼───────────ZZ^(-4/13) │ │ │ │ -│ │ ZZ──────────┼───────────ZZ^(-4/13) │ │ │ -└ │ │ │ │ │ │ ┘ -┌ │ │ │ │ │ │ ┐ -│ ZZ──────────┼───────────┼───────────┼───────────ZZ^(-4/13) │ │ -│ │ ZZ──────────┼───────────┼───────────┼───────────ZZ^(-4/13) │ -└ │ │ │ │ │ │ ┘ - Rx(0.151π) Rx(0.151π) ZZ──────────┼───────────ZZ^(-4/13) │ - │ │ │ │ │ │ - ZZ──────────ZZ^-0.941 ZZ──────────┼───────────┼───────────ZZ^(-4/13) - │ │ │ ZZ──────────ZZ^(-4/13) │ -┌ │ │ │ │ │ │ ┐ -│ │ │ Rx(0.151π) ZZ──────────┼───────────ZZ^(-4/13) │ -│ │ │ │ │ Rx(0.151π) │ │ -└ │ │ │ │ │ │ ┘ - ZZ──────────┼───────────ZZ^-0.941 Rx(0.151π) │ Rx(0.151π) -┌ │ │ │ │ │ │ ┐ -│ ZZ──────────┼───────────┼───────────┼───────────ZZ^-0.941 │ │ -│ │ ZZ──────────┼───────────ZZ^-0.941 │ │ │ -└ │ │ │ │ │ │ ┘ - Rx(-0.448π) ZZ──────────┼───────────┼───────────┼───────────ZZ^-0.941 - │ │ ZZ──────────┼───────────ZZ^-0.941 │ - │ │ │ │ │ │ - │ Rx(-0.448π) ZZ──────────┼───────────┼───────────ZZ^-0.941 - │ │ │ ZZ──────────ZZ^-0.941 │ -┌ │ │ │ │ │ │ ┐ -│ │ │ Rx(-0.448π) ZZ──────────┼───────────ZZ^-0.941 │ -│ │ │ │ │ Rx(-0.448π) │ │ -└ │ │ │ │ │ │ ┘ - │ │ │ Rx(-0.448π) │ Rx(-0.448π) - │ │ │ │ │ │ - M('m')──────M───────────M───────────M───────────M───────────M - │ │ │ │ │ │ -Optimizing objective function ... -The largest cut value found was 7. -The largest possible cut has size 7. -The approximation ratio achieved is 1.0. -""" - import itertools import numpy as np import networkx import scipy.optimize +from sympy.parsing.sympy_parser import parse_expr import cirq +import examples.hamiltonian_representation as hr -def main(repetitions=1000, maxiter=50): - # Set problem parameters - n = 6 - p = 2 - - # Generate a random 3-regular graph on n nodes - graph = networkx.random_regular_graph(3, n) +def brute_force(graph, n): + bitstrings = np.array(list(itertools.product(range(2), repeat=n))) + mat = networkx.adjacency_matrix(graph, nodelist=sorted(graph.nodes)) + vecs = (-1) ** bitstrings + vals = 0.5 * np.sum(vecs * (mat @ vecs.T).T, axis=-1) + vals = 0.5 * (graph.size() - vals) + return max(np.round(vals)) - # Make qubits - qubits = cirq.LineQubit.range(n) - # Print an example circuit - betas = np.random.uniform(-np.pi, np.pi, size=p) - gammas = np.random.uniform(-np.pi, np.pi, size=p) - circuit = qaoa_max_cut_circuit(qubits, betas, gammas, graph) - print('Example QAOA circuit:') - print(circuit.to_text_diagram(transpose=True)) +def qaoa(booleans, repetitions=10, maxiter=250, p=5): + name_to_id = hr.get_name_to_id(booleans) + hamiltonians = [hr.build_hamiltonian_from_boolean(boolean, name_to_id) for boolean in booleans] + qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] - # Create variables to store the largest cut and cut value found - largest_cut_found = None - largest_cut_value_found = 0 + def f(x): + # Build the circuit. + circuit = cirq.Circuit() + circuit.append(cirq.H.on_each(*qubits)) - # Initialize simulator - simulator = cirq.Simulator() + for i in range(p): + circuit += hr.build_circuit_from_hamiltonians(hamiltonians, qubits, 2.0 * x[p + i]) + circuit.append(cirq.rx(2.0 * x[i]).on_each(*qubits)) - # Define objective function (we'll use the negative expected cut value) + circuit.append(cirq.measure(*qubits, key='m')) - def f(x): - # Create circuit - betas = x[:p] - gammas = x[p:] - circuit = qaoa_max_cut_circuit(qubits, betas, gammas, graph) - # Sample bitstrings from circuit - result = simulator.run(circuit, repetitions=repetitions) + # Measure + result = cirq.Simulator().run(circuit, repetitions=repetitions) bitstrings = result.measurements['m'] - # Process bitstrings - nonlocal largest_cut_found - nonlocal largest_cut_value_found - values = cut_values(bitstrings, graph) - max_value_index = np.argmax(values) - max_value = values[max_value_index] - if max_value > largest_cut_value_found: - largest_cut_value_found = max_value - largest_cut_found = bitstrings[max_value_index] - mean = np.mean(values) - return -mean - - # Pick an initial guess - x0 = np.random.uniform(-np.pi, np.pi, size=2 * p) - - # Optimize f - print('Optimizing objective function ...') - scipy.optimize.minimize(f, x0, method='Nelder-Mead', options={'maxiter': maxiter}) - # Compute best possible cut value via brute force search - all_bitstrings = np.array(list(itertools.product(range(2), repeat=n))) - all_values = cut_values(all_bitstrings, graph) - max_cut_value = np.max(all_values) + # Evaluate + values = [] + for rep in range(repetitions): + subs = {name: val == 1 for name, val in zip(name_to_id.keys(), bitstrings[rep, :])} + values.append(sum(1 if boolean.subs(subs) else 0 for boolean in booleans)) - # Print the results - print(f'The largest cut value found was {largest_cut_value_found}.') - print(f'The largest possible cut has size {max_cut_value}.') - print(f'The approximation ratio achieved is {largest_cut_value_found / max_cut_value}.') + print('μ=%.2f max=%d' % (np.mean(values), max(values))) + return -np.mean(values) -def rzz(rads): - """Returns a gate with the matrix exp(-i Z⊗Z rads).""" - return cirq.ZZPowGate(exponent=2 * rads / np.pi, global_shift=-0.5) + x0 = np.zeros(2 * p) + scipy.optimize.minimize(f, x0, method='COBYLA', options={'maxiter': maxiter, 'disp': True}) -def qaoa_max_cut_unitary(qubits, betas, gammas, graph): # Nodes should be integers - for beta, gamma in zip(betas, gammas): - yield (rzz(-0.5 * gamma).on(qubits[i], qubits[j]) for i, j in graph.edges) - yield cirq.rx(2 * beta).on_each(*qubits) +def main(): + # Set problem parameters + n = 6 + # Generate a random bipartite graph. + graph = networkx.complete_multipartite_graph(n, n) -def qaoa_max_cut_circuit(qubits, betas, gammas, graph): # Nodes should be integers - return cirq.Circuit( - # Prepare uniform superposition - cirq.H.on_each(*qubits), - # Apply QAOA unitary - qaoa_max_cut_unitary(qubits, betas, gammas, graph), - # Measure - cirq.measure(*qubits, key='m'), - ) + # Compute best possible cut value via brute force search + print('Brute force max cut: %d' % (brute_force(graph, 2 * n))) + # Build the boolean expressions + booleans = [parse_expr(f"x{i} ^ x{j}") for i, j in graph.edges] -def cut_values(bitstrings, graph): - mat = networkx.adjacency_matrix(graph, nodelist=sorted(graph.nodes)) - vecs = (-1) ** bitstrings - vals = 0.5 * np.sum(vecs * (mat @ vecs.T).T, axis=-1) - vals = 0.5 * (graph.size() - vals) - return vals + qaoa(booleans) if __name__ == '__main__': From 715a7dd0e60cc0ec4c62eda216eb1096fabec9ea Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sun, 4 Apr 2021 20:36:18 +0000 Subject: [PATCH 09/87] Attempt to fix unit test --- examples/examples_test.py | 2 +- examples/qaoa.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/examples_test.py b/examples/examples_test.py index ccf49d1364c..97405fefdf5 100644 --- a/examples/examples_test.py +++ b/examples/examples_test.py @@ -97,7 +97,7 @@ def test_example_heatmaps(): def test_example_runs_qaoa(): - examples.qaoa.main(repetitions=10, maxiter=5) + examples.qaoa.main(repetitions=1, maxiter=1, p=1) def test_example_runs_quantum_teleportation(): diff --git a/examples/qaoa.py b/examples/qaoa.py index 2af26aa436f..0c7841a1c85 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -18,7 +18,7 @@ def brute_force(graph, n): return max(np.round(vals)) -def qaoa(booleans, repetitions=10, maxiter=250, p=5): +def qaoa(booleans, repetitions, maxiter, p): name_to_id = hr.get_name_to_id(booleans) hamiltonians = [hr.build_hamiltonian_from_boolean(boolean, name_to_id) for boolean in booleans] qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] @@ -52,7 +52,7 @@ def f(x): scipy.optimize.minimize(f, x0, method='COBYLA', options={'maxiter': maxiter, 'disp': True}) -def main(): +def main(repetitions=10, maxiter=250, p=5): # Set problem parameters n = 6 @@ -65,7 +65,7 @@ def main(): # Build the boolean expressions booleans = [parse_expr(f"x{i} ^ x{j}") for i, j in graph.edges] - qaoa(booleans) + qaoa(booleans, repetitions=repetitions, maxiter=maxiter, p=p) if __name__ == '__main__': From 34a1d089b63a5d4cecd2cb145698424452a815e2 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Wed, 7 Apr 2021 00:19:47 +0000 Subject: [PATCH 10/87] Sort indices --- examples/hamiltonian_representation.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 97bb429ac58..89fb72ead34 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -7,14 +7,11 @@ import cirq # References: -# [1] On the representation of Boolean and real functions as Hamil tonians for quantum computing -# by StuartHadfield +# [1] On the representation of Boolean and real functions as Hamiltonians for quantum computing +# by Stuart Hadfield # [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 -# TODO(tonybruguier): Hook up this code with the QAOA example so that we can solve generic problems -# instead of just the max-cut example. - class HamiltonianList: """A container class of Boolean function as equation (2) or [1]""" @@ -50,7 +47,7 @@ def __mul__(self, other): hamiltonians = {} for h1, w1 in self.hamiltonians.items(): for h2, w2 in other.hamiltonians.items(): - h = tuple(set(h1).symmetric_difference(h2)) + h = tuple(sorted(set(h1).symmetric_difference(h2))) w = w1 * w2 if h not in hamiltonians: hamiltonians[h] = 0 From db6a2611b301f3a7450e36531fafcef22faae3b0 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Wed, 7 Apr 2021 00:21:39 +0000 Subject: [PATCH 11/87] More comments --- examples/hamiltonian_representation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 89fb72ead34..60540af3d16 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -17,6 +17,8 @@ class HamiltonianList: """A container class of Boolean function as equation (2) or [1]""" def __init__(self, hamiltonians: Dict[Tuple[int, ...], float]): + # The representation is Tuple[int, ...] to weights. The tuple contains the integers of + # where Z_i is present. For example, Z_0.Z_3 would be (0, 3), and I is the empty tuple. self.hamiltonians = {h: w for h, w in hamiltonians.items() if math.fabs(w) > 1e-12} def __str__(self): From f2fa4976a471ad94b0c7142cb4921b3cdbc1070d Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Mon, 12 Apr 2021 04:38:26 +0000 Subject: [PATCH 12/87] Address comments --- examples/hamiltonian_representation.py | 118 +++++++++++++++----- examples/hamiltonian_representation_test.py | 16 ++- 2 files changed, 94 insertions(+), 40 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 60540af3d16..1daa1f8f14f 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -1,14 +1,16 @@ import math -from typing import Dict, Tuple +from typing import Dict, List, Sequence, Tuple from sympy.logic.boolalg import And, Not, Or, Xor +from sympy.core.expr import Expr from sympy.core.symbol import Symbol +from sympy.parsing.sympy_parser import parse_expr import cirq # References: # [1] On the representation of Boolean and real functions as Hamiltonians for quantum computing -# by Stuart Hadfield +# 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 @@ -19,36 +21,46 @@ class HamiltonianList: def __init__(self, hamiltonians: Dict[Tuple[int, ...], float]): # The representation is Tuple[int, ...] to weights. The tuple contains the integers of # where Z_i is present. For example, Z_0.Z_3 would be (0, 3), and I is the empty tuple. - self.hamiltonians = {h: w for h, w in hamiltonians.items() if math.fabs(w) > 1e-12} + self._hamiltonians = {h: w for h, w in hamiltonians.items() if math.fabs(w) > 1e-12} + + @property + def hamiltonians(self): + return self._hamiltonians def __str__(self): # For run-to-run identicalness, we sort the keys lexicographically. return "; ".join( - f"{self.hamiltonians[h]:.2f}.{'.'.join('Z_%d' % d for d in h) if h else 'I'}" - for h in sorted(self.hamiltonians) + f"{self._hamiltonians[h]:.2f}.{'.'.join('Z_%d' % d for d in h) if h else 'I'}" + for h in sorted(self._hamiltonians) ) - def __add__(self, other): + def __add__(self, other: 'HamiltonianList') -> 'HamiltonianList': return self._signed_add(other, 1.0) - def __sub__(self, other): + def __sub__(self, other: 'HamiltonianList') -> 'HamiltonianList': return self._signed_add(other, -1.0) - def _signed_add(self, other, sign: float): - hamiltonians = self.hamiltonians.copy() + def _signed_add(self, other: 'HamiltonianList', sign: float) -> 'HamiltonianList': + hamiltonians = self._hamiltonians.copy() for h, w in other.hamiltonians.items(): if h not in hamiltonians: hamiltonians[h] = 0 hamiltonians[h] += sign * w return HamiltonianList(hamiltonians) - def __rmul__(self, other: float): - return HamiltonianList({k: other * w for k, w in self.hamiltonians.items()}) + def __rmul__(self, other: float) -> 'HamiltonianList': + return HamiltonianList({k: other * w for k, w in self._hamiltonians.items()}) - def __mul__(self, other): + def __mul__(self, other: 'HamiltonianList') -> 'HamiltonianList': hamiltonians = {} - for h1, w1 in self.hamiltonians.items(): + for h1, w1 in self._hamiltonians.items(): for h2, w2 in other.hamiltonians.items(): + # Since we represent the Hamilonians using the indices of the Z_i, when we multiply + # two Hamiltionians, it's equivalent to doing an XOR of the two sets. For example, + # if h_A = Z_1 . Z_2 and h_B = Z_1 . Z_3 then the product is: + # h_A . h_B = Z_1 . Z_2 . Z_1 . Z_3 = Z_1 . Z_1 . Z_2 . Z_3 = Z_2 . Z_3 + # and thus, it is represented by (2, 3). In sort, we do the XOR / symmetric + # difference of the two tuples. h = tuple(sorted(set(h1).symmetric_difference(h2))) w = w1 * w2 if h not in hamiltonians: @@ -57,19 +69,21 @@ def __mul__(self, other): return HamiltonianList(hamiltonians) @staticmethod - def O(): + def O() -> 'HamiltonianList': return HamiltonianList({}) @staticmethod - def I(): + def I() -> 'HamiltonianList': return HamiltonianList({(): 1.0}) @staticmethod - def Z(i: int): + def Z(i: int) -> 'HamiltonianList': return HamiltonianList({(i,): 1.0}) -def build_hamiltonian_from_boolean(boolean_expr, name_to_id) -> HamiltonianList: +def build_hamiltonian_from_boolean( + boolean_expr: Expr, name_to_id: Dict[str, int] +) -> HamiltonianList: """Builds the Hamiltonian representation of Boolean expression as per [1]: Args: @@ -80,6 +94,10 @@ def build_hamiltonian_from_boolean(boolean_expr, name_to_id) -> HamiltonianList: Return: The HamiltonianList that represents the Boolean expression. """ + if isinstance(boolean_expr, Symbol): + # Table 1 of [1], entry for 'x' is '1/2.I - 1/2.Z' + i = name_to_id[boolean_expr.name] + return 0.5 * HamiltonianList.I() - 0.5 * HamiltonianList.Z(i) if isinstance(boolean_expr, (And, Not, Or, Xor)): sub_hamiltonians = [ @@ -103,15 +121,20 @@ def build_hamiltonian_from_boolean(boolean_expr, name_to_id) -> HamiltonianList: for sub_hamiltonian in sub_hamiltonians: hamiltonian = hamiltonian + sub_hamiltonian - 2.0 * hamiltonian * sub_hamiltonian return hamiltonian - elif isinstance(boolean_expr, Symbol): - # Table 1 of [1], entry for 'x' is '1/2.I - 1/2.Z' - i = name_to_id[boolean_expr.name] - return 0.5 * HamiltonianList.I() - 0.5 * HamiltonianList.Z(i) - else: - raise ValueError(f'Unsupported type: {type(boolean_expr)}') + + raise ValueError(f'Unsupported type: {type(boolean_expr)}') -def get_name_to_id(boolean_exprs): +def get_name_to_id(boolean_exprs: Sequence[Expr]) -> Dict[str, int]: + """Maps the variables to a unique integer. + + Args: + boolean_expr: A Sympy expression containing symbols and Boolean operations + + Return: + A dictionary of string (the variable name) to a unique integer. + """ + # For run-to-run identicalness, we sort the symbol name lexicographically. symbol_names = sorted( {symbol.name for boolean_expr in boolean_exprs for symbol in boolean_expr.free_symbols} @@ -119,17 +142,50 @@ def get_name_to_id(boolean_exprs): return {symbol_name: i for i, symbol_name in enumerate(symbol_names)} -def build_circuit_from_hamiltonians(hamiltonians, qubits, theta): +def build_circuit_from_hamiltonians( + hamiltonian_lists: List[HamiltonianList], qubits: List[cirq.Qid], theta: float +) -> cirq.Circuit: + """Builds a circuit according to [1]. + + Args: + hamiltonian_lists: the list of Hamiltonians, typically built by calling + build_hamiltonian_from_boolean(). + qubits: The list of qubits corresponding to the variables. + theta: A single float scaling the rotations. + + Return: + A dictionary of string (the variable name) to a unique integer. + """ circuit = cirq.Circuit() - for hamiltonian in hamiltonians: - for h, w in hamiltonian.hamiltonians.items(): - for i in range(1, len(h)): - circuit.append(cirq.CNOT(qubits[h[i]], qubits[h[0]])) + for hamiltonian_list in hamiltonian_lists: + for h, w in hamiltonian_list.hamiltonians.items(): + circuit.append([cirq.CNOT(qubits[c], qubits[h[0]]) for c in h[1:]]) if len(h) >= 1: circuit.append(cirq.Rz(rads=(theta * w)).on(qubits[h[0]])) - for i in range(1, len(h)): - circuit.append(cirq.CNOT(qubits[h[i]], qubits[h[0]])) + circuit.append([cirq.CNOT(qubits[c], qubits[h[0]]) for c in h[1:]]) return circuit + + +def build_circuit_from_boolean_expressions(boolean_exprs: Sequence[Expr], theta: float): + """Wrappers of all the functions to go from Boolean expressions to circuit. + + Args: + boolean_exprs: The list of Sympy Boolean expressions. + theta: The list of thetas to scale the + + Return: + A dictionary of string (the variable name) to a unique integer. + """ + booleans = [parse_expr(boolean_expr) for boolean_expr in boolean_exprs] + name_to_id = get_name_to_id(booleans) + + hamiltonians = [build_hamiltonian_from_boolean(boolean, name_to_id) for boolean in booleans] + + qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] + circuit = cirq.Circuit() + circuit += build_circuit_from_hamiltonians(hamiltonians, qubits, theta) + + return circuit, qubits diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index 62ad79817d0..13d40fe54e7 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -7,7 +7,7 @@ import cirq import examples.hamiltonian_representation as hr -# These are some of the entries of table 1. +# These are some of the entries of table 1 of https://arxiv.org/pdf/1804.09130.pdf. @pytest.mark.parametrize( 'boolean_expr,hamiltonian', [ @@ -45,16 +45,14 @@ def test_unsupported_op(): ], ) def test_circuit(boolean_expr, expected): - boolean = parse_expr(boolean_expr) - name_to_id = hr.get_name_to_id([boolean]) - hamiltonian = hr.build_hamiltonian_from_boolean(boolean, name_to_id) + circuit_hamiltonians, qubits = hr.build_circuit_from_boolean_expressions( + [boolean_expr], 0.1 * math.pi + ) - qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] - circuit = cirq.Circuit() - circuit.append(cirq.H.on_each(*qubits)) + circuit_hadamard = cirq.Circuit() + circuit_hadamard.append(cirq.H.on_each(*qubits)) - theta = 0.1 * math.pi - circuit += hr.build_circuit_from_hamiltonians([hamiltonian], qubits, theta) + circuit = circuit_hadamard + circuit_hamiltonians phi = cirq.Simulator().simulate(circuit, qubit_order=qubits, initial_state=0).state_vector() actual = np.arctan2(phi.real, phi.imag) - math.pi / 2.0 > 0.0 From 0e1980f442fb22a281c870b34c83f654c50603d5 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Mon, 12 Apr 2021 04:44:58 +0000 Subject: [PATCH 13/87] Make float for weights --- examples/hamiltonian_representation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 1daa1f8f14f..8dedff5bd1a 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -64,7 +64,7 @@ def __mul__(self, other: 'HamiltonianList') -> 'HamiltonianList': h = tuple(sorted(set(h1).symmetric_difference(h2))) w = w1 * w2 if h not in hamiltonians: - hamiltonians[h] = 0 + hamiltonians[h] = 0.0 hamiltonians[h] += w return HamiltonianList(hamiltonians) From e7c99ec530146b9f2dd248d9bac69e3bd39751b4 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Mon, 12 Apr 2021 04:50:01 +0000 Subject: [PATCH 14/87] Attempt to mypy fix --- examples/hamiltonian_representation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 8dedff5bd1a..3e9c2d10f8e 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -143,7 +143,7 @@ def get_name_to_id(boolean_exprs: Sequence[Expr]) -> Dict[str, int]: def build_circuit_from_hamiltonians( - hamiltonian_lists: List[HamiltonianList], qubits: List[cirq.Qid], theta: float + hamiltonian_lists: List[HamiltonianList], qubits: List[cirq.NamedQubit], theta: float ) -> cirq.Circuit: """Builds a circuit according to [1]. From d8ede306fe9abe7f59d6eb22694580ca9877ac41 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Mon, 12 Apr 2021 06:20:23 +0000 Subject: [PATCH 15/87] Faster exec --- examples/hamiltonian_representation.py | 54 ++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 3e9c2d10f8e..80ee4d91565 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -1,3 +1,4 @@ +import functools import math from typing import Dict, List, Sequence, Tuple @@ -13,6 +14,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, Alán Aspuru-Guzik. class HamiltonianList: @@ -44,7 +47,7 @@ def _signed_add(self, other: 'HamiltonianList', sign: float) -> 'HamiltonianList hamiltonians = self._hamiltonians.copy() for h, w in other.hamiltonians.items(): if h not in hamiltonians: - hamiltonians[h] = 0 + hamiltonians[h] = 0.0 hamiltonians[h] += sign * w return HamiltonianList(hamiltonians) @@ -156,15 +159,52 @@ def build_circuit_from_hamiltonians( Return: A dictionary of string (the variable name) to a unique integer. """ - circuit = cirq.Circuit() + combined = HamiltonianList.O() for hamiltonian_list in hamiltonian_lists: - for h, w in hamiltonian_list.hamiltonians.items(): - circuit.append([cirq.CNOT(qubits[c], qubits[h[0]]) for c in h[1:]]) + combined += hamiltonian_list + + circuit = cirq.Circuit() + + # Here we follow improvements of [4] cancelling out the CNOTs and ordering using a Gray code. + def _gray_code_comparator(k1, k2, flip=False): + 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) + + sorted_hs = sorted( + list(combined.hamiltonians.keys()), key=functools.cmp_to_key(_gray_code_comparator) + ) + + # Applies the CNOTs + def _apply_cnots(h): + circuit.append([cirq.CNOT(qubits[c], qubits[h[-1]]) for c in h[0:-1]]) + + previous_h = () + for h in sorted_hs: + w = combined.hamiltonians[h] + + # We first test whether the rotation is applied on the same qubit. + last_qubit_is_same = previous_h and h and previous_h[-1] == h[-1] + if last_qubit_is_same: + # Instead of applying previous_h and then h, we just apply the symmetric difference of + # the two CNOTs. + h_diff = tuple(sorted(set(previous_h).symmetric_difference(h))) + _apply_cnots(h_diff + (h[-1],)) + # This would be equivalent to the lines below, but it should use fewer gates. + else: + _apply_cnots(previous_h) + _apply_cnots(h) + + if len(h) >= 1: + circuit.append(cirq.Rz(rads=(theta * w)).on(qubits[h[-1]])) - if len(h) >= 1: - circuit.append(cirq.Rz(rads=(theta * w)).on(qubits[h[0]])) + previous_h = h - circuit.append([cirq.CNOT(qubits[c], qubits[h[0]]) for c in h[1:]]) + _apply_cnots(previous_h) return circuit From 078f91a25e840411868dc80f73d8c27390f1fdd0 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Mon, 12 Apr 2021 06:50:58 +0000 Subject: [PATCH 16/87] Better test coverage --- examples/hamiltonian_representation.py | 22 ++++++------ examples/hamiltonian_representation_test.py | 40 +++++++++++++++++++++ 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 80ee4d91565..6ccd6c529e9 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -145,6 +145,16 @@ def get_name_to_id(boolean_exprs: Sequence[Expr]) -> Dict[str, int]: return {symbol_name: i for i, symbol_name in enumerate(symbol_names)} +def _gray_code_comparator(k1, k2, flip=False): + 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 build_circuit_from_hamiltonians( hamiltonian_lists: List[HamiltonianList], qubits: List[cirq.NamedQubit], theta: float ) -> cirq.Circuit: @@ -165,16 +175,8 @@ def build_circuit_from_hamiltonians( circuit = cirq.Circuit() - # Here we follow improvements of [4] cancelling out the CNOTs and ordering using a Gray code. - def _gray_code_comparator(k1, k2, flip=False): - 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) - + # Here we follow improvements of [4] cancelling out the CNOTs. The first step is to order by + # Gray code so that as few as possible gates are changed. sorted_hs = sorted( list(combined.hamiltonians.keys()), key=functools.cmp_to_key(_gray_code_comparator) ) diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index 13d40fe54e7..3b87f8ac0cd 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -1,4 +1,6 @@ +import functools import math +import random import numpy as np import pytest @@ -58,3 +60,41 @@ def test_circuit(boolean_expr, expected): actual = np.arctan2(phi.real, phi.imag) - math.pi / 2.0 > 0.0 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(hr._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 hr._gray_code_comparator(seq_a, seq_b) == expected \ No newline at end of file From 4954bc42932f7778008e75debcd577c239bfc584 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Mon, 12 Apr 2021 06:53:58 +0000 Subject: [PATCH 17/87] nit add missing \n --- examples/hamiltonian_representation_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index 3b87f8ac0cd..17eba540f33 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -97,4 +97,4 @@ def test_gray_code_sorting(n_bits, expected_hs): ], ) def test_gray_code_comparison(seq_a, seq_b, expected): - assert hr._gray_code_comparator(seq_a, seq_b) == expected \ No newline at end of file + assert hr._gray_code_comparator(seq_a, seq_b) == expected From ae20cdf69b192cdfde1c599ce8b584335cd8d401 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Mon, 12 Apr 2021 15:35:26 +0000 Subject: [PATCH 18/87] Address more comments --- examples/hamiltonian_representation.py | 30 ++++++++++----------- examples/hamiltonian_representation_test.py | 6 ++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 6ccd6c529e9..54855691f86 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -1,11 +1,12 @@ +from collections import defaultdict import functools import math -from typing import Dict, List, Sequence, Tuple +from typing import DefaultDict, List, Sequence, Tuple from sympy.logic.boolalg import And, Not, Or, Xor from sympy.core.expr import Expr from sympy.core.symbol import Symbol -from sympy.parsing.sympy_parser import parse_expr +import sympy.parsing.sympy_parser as sympy_parser import cirq @@ -21,21 +22,24 @@ class HamiltonianList: """A container class of Boolean function as equation (2) or [1]""" - def __init__(self, hamiltonians: Dict[Tuple[int, ...], float]): + def __init__(self, hamiltonians: DefaultDict[Tuple[int, ...], float]): # The representation is Tuple[int, ...] to weights. The tuple contains the integers of # where Z_i is present. For example, Z_0.Z_3 would be (0, 3), and I is the empty tuple. - self._hamiltonians = {h: w for h, w in hamiltonians.items() if math.fabs(w) > 1e-12} + self._hamiltonians = defaultdict( + float, {h: w for h, w in hamiltonians.items() if math.fabs(w) > 1e-12} + ) @property def hamiltonians(self): return self._hamiltonians - def __str__(self): + def __repr__(self): # For run-to-run identicalness, we sort the keys lexicographically. - return "; ".join( + formatted_terms = [ f"{self._hamiltonians[h]:.2f}.{'.'.join('Z_%d' % d for d in h) if h else 'I'}" for h in sorted(self._hamiltonians) - ) + ] + return "; ".join(formatted_terms) def __add__(self, other: 'HamiltonianList') -> 'HamiltonianList': return self._signed_add(other, 1.0) @@ -46,8 +50,6 @@ def __sub__(self, other: 'HamiltonianList') -> 'HamiltonianList': def _signed_add(self, other: 'HamiltonianList', sign: float) -> 'HamiltonianList': hamiltonians = self._hamiltonians.copy() for h, w in other.hamiltonians.items(): - if h not in hamiltonians: - hamiltonians[h] = 0.0 hamiltonians[h] += sign * w return HamiltonianList(hamiltonians) @@ -55,7 +57,7 @@ def __rmul__(self, other: float) -> 'HamiltonianList': return HamiltonianList({k: other * w for k, w in self._hamiltonians.items()}) def __mul__(self, other: 'HamiltonianList') -> 'HamiltonianList': - hamiltonians = {} + hamiltonians = defaultdict(float, {}) for h1, w1 in self._hamiltonians.items(): for h2, w2 in other.hamiltonians.items(): # Since we represent the Hamilonians using the indices of the Z_i, when we multiply @@ -66,8 +68,6 @@ def __mul__(self, other: 'HamiltonianList') -> 'HamiltonianList': # difference of the two tuples. h = tuple(sorted(set(h1).symmetric_difference(h2))) w = w1 * w2 - if h not in hamiltonians: - hamiltonians[h] = 0.0 hamiltonians[h] += w return HamiltonianList(hamiltonians) @@ -85,7 +85,7 @@ def Z(i: int) -> 'HamiltonianList': def build_hamiltonian_from_boolean( - boolean_expr: Expr, name_to_id: Dict[str, int] + boolean_expr: Expr, name_to_id: DefaultDict[str, int] ) -> HamiltonianList: """Builds the Hamiltonian representation of Boolean expression as per [1]: @@ -128,7 +128,7 @@ def build_hamiltonian_from_boolean( raise ValueError(f'Unsupported type: {type(boolean_expr)}') -def get_name_to_id(boolean_exprs: Sequence[Expr]) -> Dict[str, int]: +def get_name_to_id(boolean_exprs: Sequence[Expr]) -> DefaultDict[str, int]: """Maps the variables to a unique integer. Args: @@ -221,7 +221,7 @@ def build_circuit_from_boolean_expressions(boolean_exprs: Sequence[Expr], theta: Return: A dictionary of string (the variable name) to a unique integer. """ - booleans = [parse_expr(boolean_expr) for boolean_expr in boolean_exprs] + booleans = [sympy_parser.parse_expr(boolean_expr) for boolean_expr in boolean_exprs] name_to_id = get_name_to_id(booleans) hamiltonians = [build_hamiltonian_from_boolean(boolean, name_to_id) for boolean in booleans] diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index 17eba540f33..c95fabe30a3 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -4,7 +4,7 @@ import numpy as np import pytest -from sympy.parsing.sympy_parser import parse_expr +import sympy.parsing.sympy_parser as sympy_parser import cirq import examples.hamiltonian_representation as hr @@ -22,14 +22,14 @@ ], ) def test_build_hamiltonian_from_boolean(boolean_expr, hamiltonian): - boolean = parse_expr(boolean_expr) + boolean = sympy_parser.parse_expr(boolean_expr) name_to_id = hr.get_name_to_id([boolean]) actual = hr.build_hamiltonian_from_boolean(boolean, name_to_id) assert hamiltonian == str(actual) def test_unsupported_op(): - not_a_boolean = parse_expr('x * x') + not_a_boolean = sympy_parser.parse_expr('x * x') name_to_id = hr.get_name_to_id([not_a_boolean]) with pytest.raises(ValueError, match='Unsupported type'): hr.build_hamiltonian_from_boolean(not_a_boolean, name_to_id) From f757a7776d2d5479d073a7e6da59ff8e3dc8b3bb Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Mon, 12 Apr 2021 15:51:57 +0000 Subject: [PATCH 19/87] mypy attempt at fixing --- examples/hamiltonian_representation.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 54855691f86..991ac3ee6bf 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -25,12 +25,12 @@ class HamiltonianList: def __init__(self, hamiltonians: DefaultDict[Tuple[int, ...], float]): # The representation is Tuple[int, ...] to weights. The tuple contains the integers of # where Z_i is present. For example, Z_0.Z_3 would be (0, 3), and I is the empty tuple. - self._hamiltonians = defaultdict( + self._hamiltonians: DefaultDict[Tuple[int, ...], float] = defaultdict( float, {h: w for h, w in hamiltonians.items() if math.fabs(w) > 1e-12} ) @property - def hamiltonians(self): + def hamiltonians(self) -> DefaultDict[Tuple[int, ...], float]: return self._hamiltonians def __repr__(self): @@ -48,16 +48,18 @@ def __sub__(self, other: 'HamiltonianList') -> 'HamiltonianList': return self._signed_add(other, -1.0) def _signed_add(self, other: 'HamiltonianList', sign: float) -> 'HamiltonianList': - hamiltonians = self._hamiltonians.copy() + hamiltonians: DefaultDict[Tuple[int, ...], float] = self._hamiltonians.copy() for h, w in other.hamiltonians.items(): hamiltonians[h] += sign * w return HamiltonianList(hamiltonians) def __rmul__(self, other: float) -> 'HamiltonianList': - return HamiltonianList({k: other * w for k, w in self._hamiltonians.items()}) + return HamiltonianList( + defaultdict(float, {k: other * w for k, w in self._hamiltonians.items()}) + ) def __mul__(self, other: 'HamiltonianList') -> 'HamiltonianList': - hamiltonians = defaultdict(float, {}) + hamiltonians: DefaultDict[Tuple[int, ...], float] = defaultdict(float, {}) for h1, w1 in self._hamiltonians.items(): for h2, w2 in other.hamiltonians.items(): # Since we represent the Hamilonians using the indices of the Z_i, when we multiply @@ -73,15 +75,15 @@ def __mul__(self, other: 'HamiltonianList') -> 'HamiltonianList': @staticmethod def O() -> 'HamiltonianList': - return HamiltonianList({}) + return HamiltonianList(defaultdict(float, {})) @staticmethod def I() -> 'HamiltonianList': - return HamiltonianList({(): 1.0}) + return HamiltonianList(defaultdict(float, {(): 1.0})) @staticmethod def Z(i: int) -> 'HamiltonianList': - return HamiltonianList({(i,): 1.0}) + return HamiltonianList(defaultdict(float, {(i,): 1.0})) def build_hamiltonian_from_boolean( From 05d89721d55a868a7cd71b007dfe6dbfd812383a Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Mon, 12 Apr 2021 16:07:05 +0000 Subject: [PATCH 20/87] Not a default dict --- examples/hamiltonian_representation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 991ac3ee6bf..ea880f0808e 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -1,7 +1,7 @@ from collections import defaultdict import functools import math -from typing import DefaultDict, List, Sequence, Tuple +from typing import DefaultDict, Dict, List, Sequence, Tuple from sympy.logic.boolalg import And, Not, Or, Xor from sympy.core.expr import Expr @@ -130,7 +130,7 @@ def build_hamiltonian_from_boolean( raise ValueError(f'Unsupported type: {type(boolean_expr)}') -def get_name_to_id(boolean_exprs: Sequence[Expr]) -> DefaultDict[str, int]: +def get_name_to_id(boolean_exprs: Sequence[Expr]) -> Dict[str, int]: """Maps the variables to a unique integer. Args: From 8cd10df9a1260f9cc26dfcad4c57ddbe13b54873 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Mon, 12 Apr 2021 16:28:08 +0000 Subject: [PATCH 21/87] mypy --- examples/hamiltonian_representation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index ea880f0808e..ce4fcd142ab 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -87,7 +87,7 @@ def Z(i: int) -> 'HamiltonianList': def build_hamiltonian_from_boolean( - boolean_expr: Expr, name_to_id: DefaultDict[str, int] + boolean_expr: Expr, name_to_id: Dict[str, int] ) -> HamiltonianList: """Builds the Hamiltonian representation of Boolean expression as per [1]: @@ -187,7 +187,7 @@ def build_circuit_from_hamiltonians( def _apply_cnots(h): circuit.append([cirq.CNOT(qubits[c], qubits[h[-1]]) for c in h[0:-1]]) - previous_h = () + previous_h: Tuple[int, ...] = () for h in sorted_hs: w = combined.hamiltonians[h] From 4cadd862bc50e0e80b13c141cd9ad39ec65afbad Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Tue, 13 Apr 2021 13:30:38 +0000 Subject: [PATCH 22/87] Address comments --- examples/hamiltonian_representation.py | 89 +++++++++++---------- examples/hamiltonian_representation_test.py | 6 +- 2 files changed, 51 insertions(+), 44 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index ce4fcd142ab..789b09361b2 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -19,7 +19,7 @@ # Greenbaum, Sarah Mostame, Alán Aspuru-Guzik. -class HamiltonianList: +class HamiltonianPolynomial: """A container class of Boolean function as equation (2) or [1]""" def __init__(self, hamiltonians: DefaultDict[Tuple[int, ...], float]): @@ -41,24 +41,24 @@ def __repr__(self): ] return "; ".join(formatted_terms) - def __add__(self, other: 'HamiltonianList') -> 'HamiltonianList': + def __add__(self, other: 'HamiltonianPolynomial') -> 'HamiltonianPolynomial': return self._signed_add(other, 1.0) - def __sub__(self, other: 'HamiltonianList') -> 'HamiltonianList': + def __sub__(self, other: 'HamiltonianPolynomial') -> 'HamiltonianPolynomial': return self._signed_add(other, -1.0) - def _signed_add(self, other: 'HamiltonianList', sign: float) -> 'HamiltonianList': + def _signed_add(self, other: 'HamiltonianPolynomial', sign: float) -> 'HamiltonianPolynomial': hamiltonians: DefaultDict[Tuple[int, ...], float] = self._hamiltonians.copy() for h, w in other.hamiltonians.items(): hamiltonians[h] += sign * w - return HamiltonianList(hamiltonians) + return HamiltonianPolynomial(hamiltonians) - def __rmul__(self, other: float) -> 'HamiltonianList': - return HamiltonianList( + def __rmul__(self, other: float) -> 'HamiltonianPolynomial': + return HamiltonianPolynomial( defaultdict(float, {k: other * w for k, w in self._hamiltonians.items()}) ) - def __mul__(self, other: 'HamiltonianList') -> 'HamiltonianList': + def __mul__(self, other: 'HamiltonianPolynomial') -> 'HamiltonianPolynomial': hamiltonians: DefaultDict[Tuple[int, ...], float] = defaultdict(float, {}) for h1, w1 in self._hamiltonians.items(): for h2, w2 in other.hamiltonians.items(): @@ -71,24 +71,24 @@ def __mul__(self, other: 'HamiltonianList') -> 'HamiltonianList': h = tuple(sorted(set(h1).symmetric_difference(h2))) w = w1 * w2 hamiltonians[h] += w - return HamiltonianList(hamiltonians) + return HamiltonianPolynomial(hamiltonians) @staticmethod - def O() -> 'HamiltonianList': - return HamiltonianList(defaultdict(float, {})) + def O() -> 'HamiltonianPolynomial': + return HamiltonianPolynomial(defaultdict(float, {})) @staticmethod - def I() -> 'HamiltonianList': - return HamiltonianList(defaultdict(float, {(): 1.0})) + def I() -> 'HamiltonianPolynomial': + return HamiltonianPolynomial(defaultdict(float, {(): 1.0})) @staticmethod - def Z(i: int) -> 'HamiltonianList': - return HamiltonianList(defaultdict(float, {(i,): 1.0})) + def Z(i: int) -> 'HamiltonianPolynomial': + return HamiltonianPolynomial(defaultdict(float, {(i,): 1.0})) def build_hamiltonian_from_boolean( boolean_expr: Expr, name_to_id: Dict[str, int] -) -> HamiltonianList: +) -> HamiltonianPolynomial: """Builds the Hamiltonian representation of Boolean expression as per [1]: Args: @@ -97,12 +97,12 @@ def build_hamiltonian_from_boolean( get_name_to_id(). Return: - The HamiltonianList that represents the Boolean expression. + The HamiltonianPolynomial that represents the Boolean expression. """ if isinstance(boolean_expr, Symbol): # Table 1 of [1], entry for 'x' is '1/2.I - 1/2.Z' i = name_to_id[boolean_expr.name] - return 0.5 * HamiltonianList.I() - 0.5 * HamiltonianList.Z(i) + return 0.5 * HamiltonianPolynomial.I() - 0.5 * HamiltonianPolynomial.Z(i) if isinstance(boolean_expr, (And, Not, Or, Xor)): sub_hamiltonians = [ @@ -111,18 +111,18 @@ def build_hamiltonian_from_boolean( ] # We apply the equalities of theorem 1 of [1]. if isinstance(boolean_expr, And): - hamiltonian = HamiltonianList.I() + hamiltonian = HamiltonianPolynomial.I() for sub_hamiltonian in sub_hamiltonians: hamiltonian = hamiltonian * sub_hamiltonian elif isinstance(boolean_expr, Not): assert len(sub_hamiltonians) == 1 - hamiltonian = HamiltonianList.I() - sub_hamiltonians[0] + hamiltonian = HamiltonianPolynomial.I() - sub_hamiltonians[0] elif isinstance(boolean_expr, Or): - hamiltonian = HamiltonianList.O() + hamiltonian = HamiltonianPolynomial.O() for sub_hamiltonian in sub_hamiltonians: hamiltonian = hamiltonian + sub_hamiltonian - hamiltonian * sub_hamiltonian elif isinstance(boolean_expr, Xor): - hamiltonian = HamiltonianList.O() + hamiltonian = HamiltonianPolynomial.O() for sub_hamiltonian in sub_hamiltonians: hamiltonian = hamiltonian + sub_hamiltonian - 2.0 * hamiltonian * sub_hamiltonian return hamiltonian @@ -158,12 +158,14 @@ def _gray_code_comparator(k1, k2, flip=False): def build_circuit_from_hamiltonians( - hamiltonian_lists: List[HamiltonianList], qubits: List[cirq.NamedQubit], theta: float + hamiltonian_polynomial_list: List[HamiltonianPolynomial], + qubits: List[cirq.NamedQubit], + theta: float, ) -> cirq.Circuit: """Builds a circuit according to [1]. Args: - hamiltonian_lists: the list of Hamiltonians, typically built by calling + hamiltonian_polynomial_list: the list of Hamiltonians, typically built by calling build_hamiltonian_from_boolean(). qubits: The list of qubits corresponding to the variables. theta: A single float scaling the rotations. @@ -171,9 +173,7 @@ def build_circuit_from_hamiltonians( Return: A dictionary of string (the variable name) to a unique integer. """ - combined = HamiltonianList.O() - for hamiltonian_list in hamiltonian_lists: - combined += hamiltonian_list + combined = sum(hamiltonian_polynomial_list, HamiltonianPolynomial.O()) circuit = cirq.Circuit() @@ -183,13 +183,10 @@ def build_circuit_from_hamiltonians( list(combined.hamiltonians.keys()), key=functools.cmp_to_key(_gray_code_comparator) ) - # Applies the CNOTs - def _apply_cnots(h): - circuit.append([cirq.CNOT(qubits[c], qubits[h[-1]]) for c in h[0:-1]]) - - previous_h: Tuple[int, ...] = () - for h in sorted_hs: - w = combined.hamiltonians[h] + def _apply_cnots(previous_h, h): + # This function applies in sequence the CNOTs from previous_h and then h. However, given + # that the h are sorted in Gray ordering and that some cancel each other, we can reduce the + # number of gates. See [4] for more details. # We first test whether the rotation is applied on the same qubit. last_qubit_is_same = previous_h and h and previous_h[-1] == h[-1] @@ -197,18 +194,26 @@ def _apply_cnots(h): # Instead of applying previous_h and then h, we just apply the symmetric difference of # the two CNOTs. h_diff = tuple(sorted(set(previous_h).symmetric_difference(h))) - _apply_cnots(h_diff + (h[-1],)) - # This would be equivalent to the lines below, but it should use fewer gates. + h_diff += (h[-1],) + circuit.append([cirq.CNOT(qubits[c], qubits[h_diff[-1]]) for c in h_diff[0:-1]]) else: - _apply_cnots(previous_h) - _apply_cnots(h) + # This is the fall-back, where we just apply the CNOTs without cancellations. + circuit.append([cirq.CNOT(qubits[c], qubits[previous_h[-1]]) for c in previous_h[0:-1]]) + circuit.append([cirq.CNOT(qubits[c], qubits[h[-1]]) for c in h[0:-1]]) + + previous_h: Tuple[int, ...] = () + for h in sorted_hs: + w = combined.hamiltonians[h] + + _apply_cnots(previous_h, h) if len(h) >= 1: circuit.append(cirq.Rz(rads=(theta * w)).on(qubits[h[-1]])) previous_h = h - _apply_cnots(previous_h) + # Flush the last CNOTs. + _apply_cnots(previous_h, ()) return circuit @@ -226,10 +231,12 @@ def build_circuit_from_boolean_expressions(boolean_exprs: Sequence[Expr], theta: booleans = [sympy_parser.parse_expr(boolean_expr) for boolean_expr in boolean_exprs] name_to_id = get_name_to_id(booleans) - hamiltonians = [build_hamiltonian_from_boolean(boolean, name_to_id) for boolean in booleans] + hamiltonian_polynomial_list = [ + build_hamiltonian_from_boolean(boolean, name_to_id) for boolean in booleans + ] qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] circuit = cirq.Circuit() - circuit += build_circuit_from_hamiltonians(hamiltonians, qubits, theta) + circuit += build_circuit_from_hamiltonians(hamiltonian_polynomial_list, qubits, theta) return circuit, qubits diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index c95fabe30a3..be3ce53fe2b 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -11,7 +11,7 @@ # These are some of the entries of table 1 of https://arxiv.org/pdf/1804.09130.pdf. @pytest.mark.parametrize( - 'boolean_expr,hamiltonian', + 'boolean_expr,expected_hamiltonian_polynomial', [ ('x', '0.50.I; -0.50.Z_0'), ('~x', '0.50.I; 0.50.Z_0'), @@ -21,11 +21,11 @@ ('x0 ^ x1 ^ x2', '0.50.I; -0.50.Z_0.Z_1.Z_2'), ], ) -def test_build_hamiltonian_from_boolean(boolean_expr, hamiltonian): +def test_build_hamiltonian_from_boolean(boolean_expr, expected_hamiltonian_polynomial): boolean = sympy_parser.parse_expr(boolean_expr) name_to_id = hr.get_name_to_id([boolean]) actual = hr.build_hamiltonian_from_boolean(boolean, name_to_id) - assert hamiltonian == str(actual) + assert expected_hamiltonian_polynomial == str(actual) def test_unsupported_op(): From bb59cb976d8463a39b3329563e7b9181a636b721 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Tue, 13 Apr 2021 15:39:53 +0000 Subject: [PATCH 23/87] Address comments --- examples/hamiltonian_representation.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 789b09361b2..6e1ebae3e65 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -10,17 +10,18 @@ import cirq -# References: -# [1] On the representation of Boolean and real functions as Hamiltonians for quantum computing -# 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, Alán Aspuru-Guzik. - class HamiltonianPolynomial: - """A container class of Boolean function as equation (2) or [1]""" + """A container class of Boolean function as equation (2) or [1] + + References: + [1] On the representation of Boolean and real functions as Hamiltonians for quantum computing + 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, Alán Aspuru-Guzik, https://arxiv.org/abs/1306.3991 + """ def __init__(self, hamiltonians: DefaultDict[Tuple[int, ...], float]): # The representation is Tuple[int, ...] to weights. The tuple contains the integers of From a36f8bba0a9d8b95b50695b363e5c3d4aa924038 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Thu, 15 Apr 2021 03:15:03 +0000 Subject: [PATCH 24/87] more unit tests --- examples/hamiltonian_representation_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index be3ce53fe2b..092543fc51d 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -43,7 +43,17 @@ def test_unsupported_op(): ('x0 ^ x1', [False, True, True, False]), ('x0 & x1', [False, False, False, True]), ('x0 | x1', [False, True, True, True]), + ('x0 & x1 & x2', [False, False, False, False, False, False, False, True]), + ('x0 & x1 & ~x2', [False, False, False, False, False, False, True, False]), ('x0 & ~x1 & x2', [False, False, False, False, False, True, False, False]), + ('x0 & ~x1 & ~x2', [False, False, False, False, True, False, False, False]), + ('~x0 & x1 & x2', [False, False, False, True, False, False, False, False]), + ('~x0 & x1 & ~x2', [False, False, True, False, False, False, False, False]), + ('~x0 & ~x1 & x2', [False, True, False, False, False, False, False, False]), + ('~x0 & ~x1 & ~x2', [True, False, False, False, False, False, False, False]), + ('x0 ^ x1 ^ x2', [False, True, True, False, True, False, False, True]), + ('x0 | (x1 & x2)', [False, False, False, True, True, True, True, True]), + ('x0 & (x1 | x2)', [False, False, False, False, False, True, True, True]), ], ) def test_circuit(boolean_expr, expected): From a2641fcd88a80d3f86ae240e7d3bc0b6429b313d Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Thu, 15 Apr 2021 03:44:08 +0000 Subject: [PATCH 25/87] Even more tests --- examples/hamiltonian_representation_test.py | 74 +++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index 092543fc51d..ddfbb759dec 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -54,6 +54,80 @@ def test_unsupported_op(): ('x0 ^ x1 ^ x2', [False, True, True, False, True, False, False, True]), ('x0 | (x1 & x2)', [False, False, False, True, True, True, True, True]), ('x0 & (x1 | x2)', [False, False, False, False, False, True, True, True]), + ( + '(x0 ^ x1 ^ x2) | (x2 ^ x3 ^ x4)', + [ + False, + True, + True, + False, + True, + True, + True, + True, + True, + True, + True, + True, + True, + False, + False, + True, + True, + True, + True, + True, + True, + False, + False, + True, + False, + True, + True, + False, + True, + True, + True, + True, + ], + ), + ( + '(x0 ^ x2 ^ x4) | (x1 ^ x2 ^ x3)', + [ + False, + True, + True, + True, + True, + True, + True, + False, + True, + True, + False, + True, + True, + False, + True, + True, + True, + False, + True, + True, + True, + True, + False, + True, + True, + True, + True, + False, + False, + True, + True, + True, + ], + ), ], ) def test_circuit(boolean_expr, expected): From 9d6fbaebf25f9ea278d2a1b7814783c65712042e Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 16 Apr 2021 05:43:20 +0000 Subject: [PATCH 26/87] Add an alternate way of building the CNOTs --- examples/hamiltonian_representation.py | 47 ++++--- examples/hamiltonian_representation_test.py | 133 ++++++-------------- 2 files changed, 72 insertions(+), 108 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 6e1ebae3e65..beb80dba77e 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -162,6 +162,7 @@ def build_circuit_from_hamiltonians( hamiltonian_polynomial_list: List[HamiltonianPolynomial], qubits: List[cirq.NamedQubit], theta: float, + ladder_target: bool = False, ) -> cirq.Circuit: """Builds a circuit according to [1]. @@ -170,6 +171,7 @@ def build_circuit_from_hamiltonians( build_hamiltonian_from_boolean(). qubits: The list of qubits corresponding to the variables. theta: A single float scaling the rotations. + ladder_target: Whether to use convention of figure 7a or 7b. Return: A dictionary of string (the variable name) to a unique integer. @@ -184,23 +186,33 @@ def build_circuit_from_hamiltonians( list(combined.hamiltonians.keys()), key=functools.cmp_to_key(_gray_code_comparator) ) - def _apply_cnots(previous_h, h): + def _apply_cnots(ph: Tuple[int, ...], ch: Tuple[int, ...]): # This function applies in sequence the CNOTs from previous_h and then h. However, given # that the h are sorted in Gray ordering and that some cancel each other, we can reduce the # number of gates. See [4] for more details. - # We first test whether the rotation is applied on the same qubit. - last_qubit_is_same = previous_h and h and previous_h[-1] == h[-1] - if last_qubit_is_same: - # Instead of applying previous_h and then h, we just apply the symmetric difference of - # the two CNOTs. - h_diff = tuple(sorted(set(previous_h).symmetric_difference(h))) - h_diff += (h[-1],) - circuit.append([cirq.CNOT(qubits[c], qubits[h_diff[-1]]) for c in h_diff[0:-1]]) + cnots = [] + + if ladder_target: + cnots.extend((ph[i], ph[i + 1]) for i in reversed(range(len(ph) - 1))) + cnots.extend((ch[i], ch[i + 1]) for i in range(len(ch) - 1)) + + # TODO(tonybruguier): Apply the simplifications of equations 9, 10, and 11. else: - # This is the fall-back, where we just apply the CNOTs without cancellations. - circuit.append([cirq.CNOT(qubits[c], qubits[previous_h[-1]]) for c in previous_h[0:-1]]) - circuit.append([cirq.CNOT(qubits[c], qubits[h[-1]]) for c in h[0:-1]]) + # We first test whether the rotation is applied on the same qubit. + last_qubit_is_same = ph and ch and ph[-1] == ch[-1] + if last_qubit_is_same: + # Instead of applying previous_h and then h, we just apply the symmetric difference of + # the two CNOTs. + dh = tuple(sorted(set(ph).symmetric_difference(ch))) + dh += (ch[-1],) + cnots.extend((dh[i], dh[-1]) for i in range(len(dh) - 1)) + else: + # This is the fall-back, where we just apply the CNOTs without cancellations. + cnots.extend((ph[i], ph[-1]) for i in range(len(ph) - 1)) + cnots.extend((ch[i], ch[-1]) for i in range(len(ch) - 1)) + + circuit.append(cirq.CNOT(qubits[c], qubits[t]) for c, t in cnots) previous_h: Tuple[int, ...] = () for h in sorted_hs: @@ -219,12 +231,15 @@ def _apply_cnots(previous_h, h): return circuit -def build_circuit_from_boolean_expressions(boolean_exprs: Sequence[Expr], theta: float): +def build_circuit_from_boolean_expressions( + boolean_exprs: Sequence[Expr], theta: float, ladder_target: bool = False +): """Wrappers of all the functions to go from Boolean expressions to circuit. Args: boolean_exprs: The list of Sympy Boolean expressions. - theta: The list of thetas to scale the + theta: The list of thetas to scale the Hamiltonian. + ladder_target: Whether to use convention of figure 7a or 7b. Return: A dictionary of string (the variable name) to a unique integer. @@ -238,6 +253,8 @@ def build_circuit_from_boolean_expressions(boolean_exprs: Sequence[Expr], theta: qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] circuit = cirq.Circuit() - circuit += build_circuit_from_hamiltonians(hamiltonian_polynomial_list, qubits, theta) + circuit += build_circuit_from_hamiltonians( + hamiltonian_polynomial_list, qubits, theta, ladder_target + ) return circuit, qubits diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index ddfbb759dec..62f2b59232c 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -1,4 +1,5 @@ import functools +import itertools import math import random @@ -36,103 +37,48 @@ def test_unsupported_op(): @pytest.mark.parametrize( - 'boolean_expr, expected', - [ - ('x', [False, True]), - ('~x', [True, False]), - ('x0 ^ x1', [False, True, True, False]), - ('x0 & x1', [False, False, False, True]), - ('x0 | x1', [False, True, True, True]), - ('x0 & x1 & x2', [False, False, False, False, False, False, False, True]), - ('x0 & x1 & ~x2', [False, False, False, False, False, False, True, False]), - ('x0 & ~x1 & x2', [False, False, False, False, False, True, False, False]), - ('x0 & ~x1 & ~x2', [False, False, False, False, True, False, False, False]), - ('~x0 & x1 & x2', [False, False, False, True, False, False, False, False]), - ('~x0 & x1 & ~x2', [False, False, True, False, False, False, False, False]), - ('~x0 & ~x1 & x2', [False, True, False, False, False, False, False, False]), - ('~x0 & ~x1 & ~x2', [True, False, False, False, False, False, False, False]), - ('x0 ^ x1 ^ x2', [False, True, True, False, True, False, False, True]), - ('x0 | (x1 & x2)', [False, False, False, True, True, True, True, True]), - ('x0 & (x1 | x2)', [False, False, False, False, False, True, True, True]), - ( + 'boolean_expr, ladder_target', + itertools.product( + [ + 'x', + '~x', + 'x0 ^ x1', + 'x0 & x1', + 'x0 | x1', + 'x0 & x1 & x2', + 'x0 & x1 & ~x2', + 'x0 & ~x1 & x2', + 'x0 & ~x1 & ~x2', + '~x0 & x1 & x2', + '~x0 & x1 & ~x2', + '~x0 & ~x1 & x2', + '~x0 & ~x1 & ~x2', + 'x0 ^ x1 ^ x2', + 'x0 | (x1 & x2)', + 'x0 & (x1 | x2)', '(x0 ^ x1 ^ x2) | (x2 ^ x3 ^ x4)', - [ - False, - True, - True, - False, - True, - True, - True, - True, - True, - True, - True, - True, - True, - False, - False, - True, - True, - True, - True, - True, - True, - False, - False, - True, - False, - True, - True, - False, - True, - True, - True, - True, - ], - ), - ( '(x0 ^ x2 ^ x4) | (x1 ^ x2 ^ x3)', - [ - False, - True, - True, - True, - True, - True, - True, - False, - True, - True, - False, - True, - True, - False, - True, - True, - True, - False, - True, - True, - True, - True, - False, - True, - True, - True, - True, - False, - False, - True, - True, - True, - ], - ), - ], + ], + [False, True], + ), ) -def test_circuit(boolean_expr, expected): +def test_circuit(boolean_expr, ladder_target): + # We use Sympy to evaluate the expression: + parsed_expr = sympy_parser.parse_expr(boolean_expr) + var_names = hr.get_name_to_id([parsed_expr]) + + n = len(var_names) + + expected = [] + for binary_inputs in itertools.product([0, 1], repeat=n): + subed_expr = parsed_expr + for var_name, binary_input in zip(var_names, binary_inputs): + subed_expr = subed_expr.subs(var_name, binary_input) + expected.append(subed_expr) + + # We build a circuit and look at its output state vector: circuit_hamiltonians, qubits = hr.build_circuit_from_boolean_expressions( - [boolean_expr], 0.1 * math.pi + [boolean_expr], 0.1 * math.pi, ladder_target ) circuit_hadamard = cirq.Circuit() @@ -143,6 +89,7 @@ def test_circuit(boolean_expr, expected): phi = cirq.Simulator().simulate(circuit, qubit_order=qubits, initial_state=0).state_vector() actual = np.arctan2(phi.real, phi.imag) - math.pi / 2.0 > 0.0 + # Compare the two: np.testing.assert_array_equal(actual, expected) From 9b64e6c134d7a346c5a6c42f23d4b90a020a70f6 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 16 Apr 2021 05:45:57 +0000 Subject: [PATCH 27/87] mypy --- examples/hamiltonian_representation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index beb80dba77e..3b97d4bb4c7 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -191,7 +191,7 @@ def _apply_cnots(ph: Tuple[int, ...], ch: Tuple[int, ...]): # that the h are sorted in Gray ordering and that some cancel each other, we can reduce the # number of gates. See [4] for more details. - cnots = [] + cnots: List[Tuple[int, int]] = [] if ladder_target: cnots.extend((ph[i], ph[i + 1]) for i in reversed(range(len(ph) - 1))) From 007eb2e69f4264aee913b7a81f9525b981d94e0d Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 16 Apr 2021 05:52:05 +0000 Subject: [PATCH 28/87] lint --- examples/hamiltonian_representation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 3b97d4bb4c7..e18c48349f3 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -202,8 +202,8 @@ def _apply_cnots(ph: Tuple[int, ...], ch: Tuple[int, ...]): # We first test whether the rotation is applied on the same qubit. last_qubit_is_same = ph and ch and ph[-1] == ch[-1] if last_qubit_is_same: - # Instead of applying previous_h and then h, we just apply the symmetric difference of - # the two CNOTs. + # Instead of applying previous_h and then h, we just apply the symmetric difference + # of the two CNOTs. dh = tuple(sorted(set(ph).symmetric_difference(ch))) dh += (ch[-1],) cnots.extend((dh[i], dh[-1]) for i in range(len(dh) - 1)) From d9fc1b67e52753c527d5dc652a9c8f12873aee8e Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 16 Apr 2021 06:26:24 +0000 Subject: [PATCH 29/87] Add some simplification --- examples/hamiltonian_representation.py | 45 +++++++++++++-------- examples/hamiltonian_representation_test.py | 2 +- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index e18c48349f3..56df46a4237 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -187,30 +187,39 @@ def build_circuit_from_hamiltonians( ) def _apply_cnots(ph: Tuple[int, ...], ch: Tuple[int, ...]): - # This function applies in sequence the CNOTs from previous_h and then h. However, given - # that the h are sorted in Gray ordering and that some cancel each other, we can reduce the - # number of gates. See [4] for more details. + # This function applies in sequence the CNOTs from ph and then ch. However, given that the + # h are sorted in Gray ordering and that some cancel each other, we can reduce the number + # of gates. See [4] for more details. cnots: List[Tuple[int, int]] = [] if ladder_target: cnots.extend((ph[i], ph[i + 1]) for i in reversed(range(len(ph) - 1))) cnots.extend((ch[i], ch[i + 1]) for i in range(len(ch) - 1)) - - # TODO(tonybruguier): Apply the simplifications of equations 9, 10, and 11. else: - # We first test whether the rotation is applied on the same qubit. - last_qubit_is_same = ph and ch and ph[-1] == ch[-1] - if last_qubit_is_same: - # Instead of applying previous_h and then h, we just apply the symmetric difference - # of the two CNOTs. - dh = tuple(sorted(set(ph).symmetric_difference(ch))) - dh += (ch[-1],) - cnots.extend((dh[i], dh[-1]) for i in range(len(dh) - 1)) - else: - # This is the fall-back, where we just apply the CNOTs without cancellations. - cnots.extend((ph[i], ph[-1]) for i in range(len(ph) - 1)) - cnots.extend((ch[i], ch[-1]) for i in range(len(ch) - 1)) + cnots.extend((ph[i], ph[-1]) for i in range(len(ph) - 1)) + cnots.extend((ch[i], ch[-1]) for i in range(len(ch) - 1)) + + found_simplification = True + while found_simplification: + # As per equations 9 and 10, if all the targets (resp. controls) are the same, the + # cnots commute. Further, if the control (resp. targets) are the same, the cnots can be + # simplified away. + found_simplification = False + for x, y in [(0, 1), (1, 0)]: + i = 0 + for j in range(1, len(cnots)): + if cnots[i][x] != cnots[j][x]: + # The targets (resp. control) don't match, so we reset the search. + i = j + continue + if cnots[i][y] == cnots[j][y]: + # The controls (resp. targets) are the same, so we can simplify away. + cnots = [cnots[k] for k in range(len(cnots)) if k != i and k != j] + found_simplification = True + break + + # TODO(tonybruguier): Apply the simplification of equation 11. circuit.append(cirq.CNOT(qubits[c], qubits[t]) for c, t in cnots) @@ -228,6 +237,8 @@ def _apply_cnots(ph: Tuple[int, ...], ch: Tuple[int, ...]): # Flush the last CNOTs. _apply_cnots(previous_h, ()) + print(circuit) # DO NOT SUBMIT + return circuit diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index 62f2b59232c..6011c87fcaa 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -59,7 +59,7 @@ def test_unsupported_op(): '(x0 ^ x1 ^ x2) | (x2 ^ x3 ^ x4)', '(x0 ^ x2 ^ x4) | (x1 ^ x2 ^ x3)', ], - [False, True], + [False], ), ) def test_circuit(boolean_expr, ladder_target): From abce0395c54015323dacdf1828ebd7379fb32470 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 16 Apr 2021 06:41:04 +0000 Subject: [PATCH 30/87] Re-add missing coverage --- examples/hamiltonian_representation_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index 6011c87fcaa..62f2b59232c 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -59,7 +59,7 @@ def test_unsupported_op(): '(x0 ^ x1 ^ x2) | (x2 ^ x3 ^ x4)', '(x0 ^ x2 ^ x4) | (x1 ^ x2 ^ x3)', ], - [False], + [False, True], ), ) def test_circuit(boolean_expr, ladder_target): From 5e155d9de42a45b8c68e126eef550a023c97d818 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 16 Apr 2021 07:08:32 +0000 Subject: [PATCH 31/87] nits --- examples/hamiltonian_representation.py | 2 -- examples/hamiltonian_representation_test.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 56df46a4237..a675aefcc38 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -237,8 +237,6 @@ def _apply_cnots(ph: Tuple[int, ...], ch: Tuple[int, ...]): # Flush the last CNOTs. _apply_cnots(previous_h, ()) - print(circuit) # DO NOT SUBMIT - return circuit diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index 62f2b59232c..13e4a3e1744 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -58,6 +58,7 @@ def test_unsupported_op(): 'x0 & (x1 | x2)', '(x0 ^ x1 ^ x2) | (x2 ^ x3 ^ x4)', '(x0 ^ x2 ^ x4) | (x1 ^ x2 ^ x3)', + 'x0 & x1 & (x2 | x3)', ], [False, True], ), From f199f1f4e5e784123a5f4ea8d985232df757c8ef Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 17 Apr 2021 05:06:20 +0000 Subject: [PATCH 32/87] Implement equation 11. --- examples/hamiltonian_representation.py | 59 +++++++++++++++++++-- examples/hamiltonian_representation_test.py | 1 + 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index a675aefcc38..0e4ee37fdb5 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -200,8 +200,7 @@ def _apply_cnots(ph: Tuple[int, ...], ch: Tuple[int, ...]): cnots.extend((ph[i], ph[-1]) for i in range(len(ph) - 1)) cnots.extend((ch[i], ch[-1]) for i in range(len(ch) - 1)) - found_simplification = True - while found_simplification: + while True: # As per equations 9 and 10, if all the targets (resp. controls) are the same, the # cnots commute. Further, if the control (resp. targets) are the same, the cnots can be # simplified away. @@ -219,7 +218,61 @@ def _apply_cnots(ph: Tuple[int, ...], ch: Tuple[int, ...]): found_simplification = True break - # TODO(tonybruguier): Apply the simplification of equation 11. + if found_simplification: + continue + + # Here we apply the simplification of equation 11. Note that by flipping the control + # and target qubits, we have an equally valid identity: + # CNOT(i, j) @ CNOT(j, k) == CNOT(j, k) @ CNOT(i, k) @ CNOT(i, j) + # CNOT(j, i) @ CNOT(k, j) == CNOT(k, j) @ CNOT(k, i) @ CNOT(j, i) + # + # Which are represented by (x, y) = (0, 1) and (x, y) = (1, 0), respectively. + for x, y in [(0, 1), (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. triggers) 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][y] != cnots[j][y]: + break + # We take a note of the trigger (resp. target). + common_A[cnots[i][x]] = i + + # Next, we look forward for as long as the triggers (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][x] != cnots[k][x]: + break + # We take a note of the target (resp. trigger). + common_B[cnots[k][y]] = 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] + found_simplification = True + break + + if found_simplification: + break + if found_simplification: + break + + if found_simplification: + continue + + break circuit.append(cirq.CNOT(qubits[c], qubits[t]) for c, t in cnots) diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index 13e4a3e1744..b71a2e3c570 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -59,6 +59,7 @@ def test_unsupported_op(): '(x0 ^ x1 ^ x2) | (x2 ^ x3 ^ x4)', '(x0 ^ x2 ^ x4) | (x1 ^ x2 ^ x3)', 'x0 & x1 & (x2 | x3)', + '(x2 | x1) ^ x0', ], [False, True], ), From 7057d9ba00ddc8ef300226e804a18f3d3752851f Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Tue, 20 Apr 2021 05:10:17 +0000 Subject: [PATCH 33/87] Address comments --- examples/hamiltonian_representation.py | 49 +++++++++++++++----------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 0e4ee37fdb5..e4c0ee7088c 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -186,26 +186,18 @@ def build_circuit_from_hamiltonians( list(combined.hamiltonians.keys()), key=functools.cmp_to_key(_gray_code_comparator) ) - def _apply_cnots(ph: Tuple[int, ...], ch: Tuple[int, ...]): - # This function applies in sequence the CNOTs from ph and then ch. However, given that the - # h are sorted in Gray ordering and that some cancel each other, we can reduce the number - # of gates. See [4] for more details. - - cnots: List[Tuple[int, int]] = [] - - if ladder_target: - cnots.extend((ph[i], ph[i + 1]) for i in reversed(range(len(ph) - 1))) - cnots.extend((ch[i], ch[i + 1]) for i in range(len(ch) - 1)) - else: - cnots.extend((ph[i], ph[-1]) for i in range(len(ph) - 1)) - cnots.extend((ch[i], ch[-1]) for i in range(len(ch) - 1)) + def _simplify_cnots(cnots): + _control = 0 + _target = 1 while True: - # As per equations 9 and 10, if all the targets (resp. controls) are the same, the - # cnots commute. Further, if the control (resp. targets) are the same, the cnots can be - # simplified away. + # As per equations 9 and 10 of [4], if all the targets (resp. controls) are the same, + # the cnots commute. Further, if the control (resp. targets) are the same, the cnots + # can be simplified away: + # CNOT(i, j) @ CNOT(k, j) = CNOT(k, j) @ CNOT(i, j) + # CNOT(i, k) @ CNOT(i, j) = CNOT(i, j) @ CNOT(i, k) found_simplification = False - for x, y in [(0, 1), (1, 0)]: + for x, y in [(_control, _target), (_target, _control)]: i = 0 for j in range(1, len(cnots)): if cnots[i][x] != cnots[j][x]: @@ -225,9 +217,7 @@ def _apply_cnots(ph: Tuple[int, ...], ch: Tuple[int, ...]): # and target qubits, we have an equally valid identity: # CNOT(i, j) @ CNOT(j, k) == CNOT(j, k) @ CNOT(i, k) @ CNOT(i, j) # CNOT(j, i) @ CNOT(k, j) == CNOT(k, j) @ CNOT(k, i) @ CNOT(j, i) - # - # Which are represented by (x, y) = (0, 1) and (x, y) = (1, 0), respectively. - for x, y in [(0, 1), (1, 0)]: + for x, y in [(_control, _target), (_target, _control)]: # We investigate potential pivots sequentially. for j in range(1, len(cnots) - 1): # First, we look back for as long as the targets (resp. triggers) are the same. @@ -271,9 +261,26 @@ def _apply_cnots(ph: Tuple[int, ...], ch: Tuple[int, ...]): if found_simplification: continue - break + return cnots + + def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): + # This function applies in sequence the CNOTs from prevh and then currh. However, given + # that the h are sorted in Gray ordering and that some cancel each other, we can reduce + # the number of gates. See [4] for more details. + + cnots: List[Tuple[int, int]] = [] + + if ladder_target: + cnots.extend((prevh[i], prevh[i + 1]) for i in reversed(range(len(prevh) - 1))) + cnots.extend((currh[i], currh[i + 1]) for i in range(len(currh) - 1)) + else: + 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)) + + cnots = _simplify_cnots(cnots) + circuit.append(cirq.CNOT(qubits[c], qubits[t]) for c, t in cnots) previous_h: Tuple[int, ...] = () From dfc3ffcd831e5aaa0b9b5a3bedd2e2ec609a9bac Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Thu, 22 Apr 2021 00:02:49 +0000 Subject: [PATCH 34/87] Address comments --- examples/hamiltonian_representation.py | 10 ++++++++-- examples/hamiltonian_representation_test.py | 2 +- examples/qaoa.py | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index e4c0ee7088c..ca72baf6805 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -199,17 +199,23 @@ def _simplify_cnots(cnots): found_simplification = False for x, y in [(_control, _target), (_target, _control)]: i = 0 + qubit_to_index: Dict[int, int] = {} for j in range(1, len(cnots)): if cnots[i][x] != cnots[j][x]: # The targets (resp. control) don't match, so we reset the search. i = j + qubit_to_index = {cnots[j][y]: j} continue - if cnots[i][y] == cnots[j][y]: + + if cnots[j][y] in qubit_to_index: + k = qubit_to_index[cnots[j][y]] # The controls (resp. targets) are the same, so we can simplify away. - cnots = [cnots[k] for k in range(len(cnots)) if k != i and k != j] + cnots = [cnots[n] for n in range(len(cnots)) if n != j and n != k] found_simplification = True break + qubit_to_index[cnots[j][y]] = j + if found_simplification: continue diff --git a/examples/hamiltonian_representation_test.py b/examples/hamiltonian_representation_test.py index b71a2e3c570..1a823d3b579 100644 --- a/examples/hamiltonian_representation_test.py +++ b/examples/hamiltonian_representation_test.py @@ -76,7 +76,7 @@ def test_circuit(boolean_expr, ladder_target): subed_expr = parsed_expr for var_name, binary_input in zip(var_names, binary_inputs): subed_expr = subed_expr.subs(var_name, binary_input) - expected.append(subed_expr) + expected.append(bool(subed_expr)) # We build a circuit and look at its output state vector: circuit_hamiltonians, qubits = hr.build_circuit_from_boolean_expressions( diff --git a/examples/qaoa.py b/examples/qaoa.py index 0c7841a1c85..ea29d148e88 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -1,3 +1,4 @@ +"""Runs the Quantum Approximate Optimization Algorithm on Max-Cut.""" import itertools import numpy as np From 5338e6105ee33c0726a94a193eba6ac2b2292322 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Wed, 5 May 2021 03:13:37 +0000 Subject: [PATCH 35/87] Add a TODO --- examples/hamiltonian_representation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index ca72baf6805..396cca545dd 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -11,6 +11,9 @@ import cirq +# TODO(tonybruguier): Consider making a stand-alone gate given a Boolean expression. + + class HamiltonianPolynomial: """A container class of Boolean function as equation (2) or [1] From cb6da1932d0e047259ecae63e66448191e2c6f80 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 15 May 2021 03:10:05 +0000 Subject: [PATCH 36/87] Revert some files --- examples/examples_test.py | 2 +- examples/qaoa.py | 169 ++++++++++++++++++++++++++++---------- 2 files changed, 127 insertions(+), 44 deletions(-) diff --git a/examples/examples_test.py b/examples/examples_test.py index d3e77f99f38..0ffc2e57e16 100644 --- a/examples/examples_test.py +++ b/examples/examples_test.py @@ -97,7 +97,7 @@ def test_example_heatmaps(): def test_example_runs_qaoa(): - examples.qaoa.main(repetitions=1, maxiter=1, p=1) + examples.qaoa.main(repetitions=10, maxiter=5) def test_example_runs_quantum_teleportation(): diff --git a/examples/qaoa.py b/examples/qaoa.py index ea29d148e88..87528dc25ac 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -1,72 +1,155 @@ -"""Runs the Quantum Approximate Optimization Algorithm on Max-Cut.""" +"""Runs the Quantum Approximate Optimization Algorithm on Max-Cut. + +=== EXAMPLE OUTPUT === + +Example QAOA circuit: + 0 1 2 3 4 5 + │ │ │ │ │ │ + H H H H H H + │ │ │ │ │ │ + ZZ──────────ZZ^(-4/13) │ │ │ │ +┌ │ │ │ │ │ │ ┐ +│ ZZ──────────┼───────────ZZ^(-4/13) │ │ │ │ +│ │ ZZ──────────┼───────────ZZ^(-4/13) │ │ │ +└ │ │ │ │ │ │ ┘ +┌ │ │ │ │ │ │ ┐ +│ ZZ──────────┼───────────┼───────────┼───────────ZZ^(-4/13) │ │ +│ │ ZZ──────────┼───────────┼───────────┼───────────ZZ^(-4/13) │ +└ │ │ │ │ │ │ ┘ + Rx(0.151π) Rx(0.151π) ZZ──────────┼───────────ZZ^(-4/13) │ + │ │ │ │ │ │ + ZZ──────────ZZ^-0.941 ZZ──────────┼───────────┼───────────ZZ^(-4/13) + │ │ │ ZZ──────────ZZ^(-4/13) │ +┌ │ │ │ │ │ │ ┐ +│ │ │ Rx(0.151π) ZZ──────────┼───────────ZZ^(-4/13) │ +│ │ │ │ │ Rx(0.151π) │ │ +└ │ │ │ │ │ │ ┘ + ZZ──────────┼───────────ZZ^-0.941 Rx(0.151π) │ Rx(0.151π) +┌ │ │ │ │ │ │ ┐ +│ ZZ──────────┼───────────┼───────────┼───────────ZZ^-0.941 │ │ +│ │ ZZ──────────┼───────────ZZ^-0.941 │ │ │ +└ │ │ │ │ │ │ ┘ + Rx(-0.448π) ZZ──────────┼───────────┼───────────┼───────────ZZ^-0.941 + │ │ ZZ──────────┼───────────ZZ^-0.941 │ + │ │ │ │ │ │ + │ Rx(-0.448π) ZZ──────────┼───────────┼───────────ZZ^-0.941 + │ │ │ ZZ──────────ZZ^-0.941 │ +┌ │ │ │ │ │ │ ┐ +│ │ │ Rx(-0.448π) ZZ──────────┼───────────ZZ^-0.941 │ +│ │ │ │ │ Rx(-0.448π) │ │ +└ │ │ │ │ │ │ ┘ + │ │ │ Rx(-0.448π) │ Rx(-0.448π) + │ │ │ │ │ │ + M('m')──────M───────────M───────────M───────────M───────────M + │ │ │ │ │ │ +Optimizing objective function ... +The largest cut value found was 7. +The largest possible cut has size 7. +The approximation ratio achieved is 1.0. +""" + import itertools import numpy as np import networkx import scipy.optimize -from sympy.parsing.sympy_parser import parse_expr import cirq -import examples.hamiltonian_representation as hr -def brute_force(graph, n): - bitstrings = np.array(list(itertools.product(range(2), repeat=n))) - mat = networkx.adjacency_matrix(graph, nodelist=sorted(graph.nodes)) - vecs = (-1) ** bitstrings - vals = 0.5 * np.sum(vecs * (mat @ vecs.T).T, axis=-1) - vals = 0.5 * (graph.size() - vals) - return max(np.round(vals)) +def main(repetitions=1000, maxiter=50): + # Set problem parameters + n = 6 + p = 2 + # Generate a random 3-regular graph on n nodes + graph = networkx.random_regular_graph(3, n) -def qaoa(booleans, repetitions, maxiter, p): - name_to_id = hr.get_name_to_id(booleans) - hamiltonians = [hr.build_hamiltonian_from_boolean(boolean, name_to_id) for boolean in booleans] - qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] + # Make qubits + qubits = cirq.LineQubit.range(n) - def f(x): - # Build the circuit. - circuit = cirq.Circuit() - circuit.append(cirq.H.on_each(*qubits)) + # Print an example circuit + betas = np.random.uniform(-np.pi, np.pi, size=p) + gammas = np.random.uniform(-np.pi, np.pi, size=p) + circuit = qaoa_max_cut_circuit(qubits, betas, gammas, graph) + print('Example QAOA circuit:') + print(circuit.to_text_diagram(transpose=True)) - for i in range(p): - circuit += hr.build_circuit_from_hamiltonians(hamiltonians, qubits, 2.0 * x[p + i]) - circuit.append(cirq.rx(2.0 * x[i]).on_each(*qubits)) + # Create variables to store the largest cut and cut value found + largest_cut_found = None + largest_cut_value_found = 0 - circuit.append(cirq.measure(*qubits, key='m')) + # Initialize simulator + simulator = cirq.Simulator() - # Measure - result = cirq.Simulator().run(circuit, repetitions=repetitions) + # Define objective function (we'll use the negative expected cut value) + + def f(x): + # Create circuit + betas = x[:p] + gammas = x[p:] + circuit = qaoa_max_cut_circuit(qubits, betas, gammas, graph) + # Sample bitstrings from circuit + result = simulator.run(circuit, repetitions=repetitions) bitstrings = result.measurements['m'] + # Process bitstrings + nonlocal largest_cut_found + nonlocal largest_cut_value_found + values = cut_values(bitstrings, graph) + max_value_index = np.argmax(values) + max_value = values[max_value_index] + if max_value > largest_cut_value_found: + largest_cut_value_found = max_value + largest_cut_found = bitstrings[max_value_index] + mean = np.mean(values) + return -mean + + # Pick an initial guess + x0 = np.random.uniform(-np.pi, np.pi, size=2 * p) + + # Optimize f + print('Optimizing objective function ...') + scipy.optimize.minimize(f, x0, method='Nelder-Mead', options={'maxiter': maxiter}) - # Evaluate - values = [] - for rep in range(repetitions): - subs = {name: val == 1 for name, val in zip(name_to_id.keys(), bitstrings[rep, :])} - values.append(sum(1 if boolean.subs(subs) else 0 for boolean in booleans)) + # Compute best possible cut value via brute force search + all_bitstrings = np.array(list(itertools.product(range(2), repeat=n))) + all_values = cut_values(all_bitstrings, graph) + max_cut_value = np.max(all_values) - print('μ=%.2f max=%d' % (np.mean(values), max(values))) + # Print the results + print(f'The largest cut value found was {largest_cut_value_found}.') + print(f'The largest possible cut has size {max_cut_value}.') + print(f'The approximation ratio achieved is {largest_cut_value_found / max_cut_value}.') - return -np.mean(values) - x0 = np.zeros(2 * p) - scipy.optimize.minimize(f, x0, method='COBYLA', options={'maxiter': maxiter, 'disp': True}) +def rzz(rads): + """Returns a gate with the matrix exp(-i Z⊗Z rads).""" + return cirq.ZZPowGate(exponent=2 * rads / np.pi, global_shift=-0.5) -def main(repetitions=10, maxiter=250, p=5): - # Set problem parameters - n = 6 +def qaoa_max_cut_unitary(qubits, betas, gammas, graph): # Nodes should be integers + for beta, gamma in zip(betas, gammas): + yield (rzz(-0.5 * gamma).on(qubits[i], qubits[j]) for i, j in graph.edges) + yield cirq.rx(2 * beta).on_each(*qubits) - # Generate a random bipartite graph. - graph = networkx.complete_multipartite_graph(n, n) - # Compute best possible cut value via brute force search - print('Brute force max cut: %d' % (brute_force(graph, 2 * n))) +def qaoa_max_cut_circuit(qubits, betas, gammas, graph): # Nodes should be integers + return cirq.Circuit( + # Prepare uniform superposition + cirq.H.on_each(*qubits), + # Apply QAOA unitary + qaoa_max_cut_unitary(qubits, betas, gammas, graph), + # Measure + cirq.measure(*qubits, key='m'), + ) - # Build the boolean expressions - booleans = [parse_expr(f"x{i} ^ x{j}") for i, j in graph.edges] - qaoa(booleans, repetitions=repetitions, maxiter=maxiter, p=p) +def cut_values(bitstrings, graph): + mat = networkx.adjacency_matrix(graph, nodelist=sorted(graph.nodes)) + vecs = (-1) ** bitstrings + vals = 0.5 * np.sum(vecs * (mat @ vecs.T).T, axis=-1) + vals = 0.5 * (graph.size() - vals) + return vals if __name__ == '__main__': From 167a1db0e3ce976f74e2365c5e2f5d611ed5aa1f Mon Sep 17 00:00:00 2001 From: "Antoine (Tony) Bruguier" Date: Fri, 14 May 2021 20:12:58 -0700 Subject: [PATCH 37/87] Update examples/hamiltonian_representation.py Co-authored-by: Balint Pato --- examples/hamiltonian_representation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/hamiltonian_representation.py b/examples/hamiltonian_representation.py index 396cca545dd..75bde09e295 100644 --- a/examples/hamiltonian_representation.py +++ b/examples/hamiltonian_representation.py @@ -15,7 +15,7 @@ class HamiltonianPolynomial: - """A container class of Boolean function as equation (2) or [1] + """A container class of Boolean function as equation (2) of [1] References: [1] On the representation of Boolean and real functions as Hamiltonians for quantum computing From 64032942f62300c677d1679f9b024fcdb949c79c Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 15 May 2021 03:27:59 +0000 Subject: [PATCH 38/87] Rename file --- {examples => cirq-core/cirq/ops}/hamiltonian_representation.py | 0 .../cirq/ops}/hamiltonian_representation_test.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {examples => cirq-core/cirq/ops}/hamiltonian_representation.py (100%) rename {examples => cirq-core/cirq/ops}/hamiltonian_representation_test.py (100%) diff --git a/examples/hamiltonian_representation.py b/cirq-core/cirq/ops/hamiltonian_representation.py similarity index 100% rename from examples/hamiltonian_representation.py rename to cirq-core/cirq/ops/hamiltonian_representation.py diff --git a/examples/hamiltonian_representation_test.py b/cirq-core/cirq/ops/hamiltonian_representation_test.py similarity index 100% rename from examples/hamiltonian_representation_test.py rename to cirq-core/cirq/ops/hamiltonian_representation_test.py From f4e765841511faa244b4f5980d2d147bbf8385c0 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 15 May 2021 03:29:58 +0000 Subject: [PATCH 39/87] Rename file --- .../ops/{hamiltonian_representation.py => hamiltonian_gate.py} | 0 ...amiltonian_representation_test.py => hamiltonian_gate_test.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename cirq-core/cirq/ops/{hamiltonian_representation.py => hamiltonian_gate.py} (100%) rename cirq-core/cirq/ops/{hamiltonian_representation_test.py => hamiltonian_gate_test.py} (100%) diff --git a/cirq-core/cirq/ops/hamiltonian_representation.py b/cirq-core/cirq/ops/hamiltonian_gate.py similarity index 100% rename from cirq-core/cirq/ops/hamiltonian_representation.py rename to cirq-core/cirq/ops/hamiltonian_gate.py diff --git a/cirq-core/cirq/ops/hamiltonian_representation_test.py b/cirq-core/cirq/ops/hamiltonian_gate_test.py similarity index 100% rename from cirq-core/cirq/ops/hamiltonian_representation_test.py rename to cirq-core/cirq/ops/hamiltonian_gate_test.py From 396ea473343677f78c435be73b36209113696658 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 15 May 2021 06:03:03 +0000 Subject: [PATCH 40/87] Make a gate --- cirq-core/cirq/__init__.py | 1 + cirq-core/cirq/ops/__init__.py | 4 + cirq-core/cirq/ops/hamiltonian_gate.py | 119 ++++++++++---------- cirq-core/cirq/ops/hamiltonian_gate_test.py | 40 +++---- 4 files changed, 86 insertions(+), 78 deletions(-) diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index 350ab8c5837..54cdbe270d4 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -209,6 +209,7 @@ givens, GlobalPhaseOperation, H, + HamiltonianGate, HPowGate, I, identity_each, diff --git a/cirq-core/cirq/ops/__init__.py b/cirq-core/cirq/ops/__init__.py index 1be292eb516..bb1d8f891d8 100644 --- a/cirq-core/cirq/ops/__init__.py +++ b/cirq-core/cirq/ops/__init__.py @@ -105,6 +105,10 @@ GateOperation, ) +from cirq.ops.hamiltonian_gate import ( + HamiltonianGate, +) + from cirq.ops.identity import ( I, identity_each, diff --git a/cirq-core/cirq/ops/hamiltonian_gate.py b/cirq-core/cirq/ops/hamiltonian_gate.py index 75bde09e295..73d34b4ddb2 100644 --- a/cirq-core/cirq/ops/hamiltonian_gate.py +++ b/cirq-core/cirq/ops/hamiltonian_gate.py @@ -6,12 +6,10 @@ from sympy.logic.boolalg import And, Not, Or, Xor from sympy.core.expr import Expr from sympy.core.symbol import Symbol -import sympy.parsing.sympy_parser as sympy_parser import cirq - - -# TODO(tonybruguier): Consider making a stand-alone gate given a Boolean expression. +from cirq import value +from cirq.ops import raw_types class HamiltonianPolynomial: @@ -90,7 +88,7 @@ def Z(i: int) -> 'HamiltonianPolynomial': return HamiltonianPolynomial(defaultdict(float, {(i,): 1.0})) -def build_hamiltonian_from_boolean( +def _build_hamiltonian_from_boolean( boolean_expr: Expr, name_to_id: Dict[str, int] ) -> HamiltonianPolynomial: """Builds the Hamiltonian representation of Boolean expression as per [1]: @@ -110,7 +108,7 @@ def build_hamiltonian_from_boolean( if isinstance(boolean_expr, (And, Not, Or, Xor)): sub_hamiltonians = [ - build_hamiltonian_from_boolean(sub_boolean_expr, name_to_id) + _build_hamiltonian_from_boolean(sub_boolean_expr, name_to_id) for sub_boolean_expr in boolean_expr.args ] # We apply the equalities of theorem 1 of [1]. @@ -134,23 +132,6 @@ def build_hamiltonian_from_boolean( raise ValueError(f'Unsupported type: {type(boolean_expr)}') -def get_name_to_id(boolean_exprs: Sequence[Expr]) -> Dict[str, int]: - """Maps the variables to a unique integer. - - Args: - boolean_expr: A Sympy expression containing symbols and Boolean operations - - Return: - A dictionary of string (the variable name) to a unique integer. - """ - - # For run-to-run identicalness, we sort the symbol name lexicographically. - symbol_names = sorted( - {symbol.name for boolean_expr in boolean_exprs for symbol in boolean_expr.free_symbols} - ) - return {symbol_name: i for i, symbol_name in enumerate(symbol_names)} - - def _gray_code_comparator(k1, k2, flip=False): max_1 = k1[-1] if k1 else -1 max_2 = k2[-1] if k2 else -1 @@ -161,28 +142,26 @@ def _gray_code_comparator(k1, k2, flip=False): return _gray_code_comparator(k1[0:-1], k2[0:-1], not flip) -def build_circuit_from_hamiltonians( +def get_gates_from_hamiltonians( hamiltonian_polynomial_list: List[HamiltonianPolynomial], - qubits: List[cirq.NamedQubit], + qubits, theta: float, ladder_target: bool = False, -) -> cirq.Circuit: +): """Builds a circuit according to [1]. Args: hamiltonian_polynomial_list: the list of Hamiltonians, typically built by calling - build_hamiltonian_from_boolean(). + _build_hamiltonian_from_boolean(). qubits: The list of qubits corresponding to the variables. theta: A single float scaling the rotations. ladder_target: Whether to use convention of figure 7a or 7b. - Return: - A dictionary of string (the variable name) to a unique integer. + Yield: + Gates that are the decomposition of the Hamiltonian. """ combined = sum(hamiltonian_polynomial_list, HamiltonianPolynomial.O()) - circuit = cirq.Circuit() - # Here we follow improvements of [4] cancelling out the CNOTs. The first step is to order by # Gray code so that as few as possible gates are changed. sorted_hs = sorted( @@ -290,49 +269,71 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): cnots = _simplify_cnots(cnots) - circuit.append(cirq.CNOT(qubits[c], qubits[t]) for c, t in cnots) + for gate in (cirq.CNOT(qubits[c], qubits[t]) for c, t in cnots): + yield gate previous_h: Tuple[int, ...] = () for h in sorted_hs: w = combined.hamiltonians[h] - _apply_cnots(previous_h, h) + yield _apply_cnots(previous_h, h) if len(h) >= 1: - circuit.append(cirq.Rz(rads=(theta * w)).on(qubits[h[-1]])) + yield cirq.Rz(rads=(theta * w)).on(qubits[h[-1]]) previous_h = h # Flush the last CNOTs. - _apply_cnots(previous_h, ()) + yield _apply_cnots(previous_h, ()) + + +@value.value_equality +class HamiltonianGate(raw_types.Gate): + """A gate that applies an Hamiltonian from a set of Boolean functions.""" + + def __init__(self, boolean_exprs: Sequence[Expr], theta: float, ladder_target: bool): + """ + Builds an HamiltonianGate. + + Args: + boolean_exprs: The list of Sympy Boolean expressions. + theta: The list of thetas to scale the Hamiltonian. + ladder_target: Whether to use convention of figure 7a or 7b. + """ + self._boolean_exprs: Sequence[Expr] = boolean_exprs + self._theta: float = theta + self._ladder_target: bool = ladder_target + + self._name_to_id = HamiltonianGate.get_name_to_id(boolean_exprs) + self._hamiltonian_polynomial_list = [ + _build_hamiltonian_from_boolean(boolean, self._name_to_id) + for boolean in self._boolean_exprs + ] - return circuit + def num_qubits(self) -> int: + return len(self._name_to_id) + @staticmethod + def get_name_to_id(boolean_exprs: Sequence[Expr]) -> Dict[str, int]: + """Maps the variables to a unique integer. -def build_circuit_from_boolean_expressions( - boolean_exprs: Sequence[Expr], theta: float, ladder_target: bool = False -): - """Wrappers of all the functions to go from Boolean expressions to circuit. + Args: + boolean_expr: A Sympy expression containing symbols and Boolean operations - Args: - boolean_exprs: The list of Sympy Boolean expressions. - theta: The list of thetas to scale the Hamiltonian. - ladder_target: Whether to use convention of figure 7a or 7b. + Return: + A dictionary of string (the variable name) to a unique integer. + """ - Return: - A dictionary of string (the variable name) to a unique integer. - """ - booleans = [sympy_parser.parse_expr(boolean_expr) for boolean_expr in boolean_exprs] - name_to_id = get_name_to_id(booleans) - - hamiltonian_polynomial_list = [ - build_hamiltonian_from_boolean(boolean, name_to_id) for boolean in booleans - ] + # For run-to-run identicalness, we sort the symbol name lexicographically. + symbol_names = sorted( + {symbol.name for boolean_expr in boolean_exprs for symbol in boolean_expr.free_symbols} + ) + return {symbol_name: i for i, symbol_name in enumerate(symbol_names)} - qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] - circuit = cirq.Circuit() - circuit += build_circuit_from_hamiltonians( - hamiltonian_polynomial_list, qubits, theta, ladder_target - ) + def _value_equality_values_(self): + return self._boolean_exprs, self._theta, self._ladder_target - return circuit, qubits + def _decompose_(self, qubits): + yield get_gates_from_hamiltonians( + self._hamiltonian_polynomial_list, qubits, self._theta, self._ladder_target + ) diff --git a/cirq-core/cirq/ops/hamiltonian_gate_test.py b/cirq-core/cirq/ops/hamiltonian_gate_test.py index 1a823d3b579..63c2d6a1cad 100644 --- a/cirq-core/cirq/ops/hamiltonian_gate_test.py +++ b/cirq-core/cirq/ops/hamiltonian_gate_test.py @@ -8,7 +8,7 @@ import sympy.parsing.sympy_parser as sympy_parser import cirq -import examples.hamiltonian_representation as hr +import cirq.ops.hamiltonian_gate as hg # These are some of the entries of table 1 of https://arxiv.org/pdf/1804.09130.pdf. @pytest.mark.parametrize( @@ -24,20 +24,20 @@ ) def test_build_hamiltonian_from_boolean(boolean_expr, expected_hamiltonian_polynomial): boolean = sympy_parser.parse_expr(boolean_expr) - name_to_id = hr.get_name_to_id([boolean]) - actual = hr.build_hamiltonian_from_boolean(boolean, name_to_id) + name_to_id = cirq.HamiltonianGate.get_name_to_id([boolean]) + actual = hg._build_hamiltonian_from_boolean(boolean, name_to_id) assert expected_hamiltonian_polynomial == str(actual) def test_unsupported_op(): not_a_boolean = sympy_parser.parse_expr('x * x') - name_to_id = hr.get_name_to_id([not_a_boolean]) + name_to_id = cirq.HamiltonianGate.get_name_to_id([not_a_boolean]) with pytest.raises(ValueError, match='Unsupported type'): - hr.build_hamiltonian_from_boolean(not_a_boolean, name_to_id) + hg._build_hamiltonian_from_boolean(not_a_boolean, name_to_id) @pytest.mark.parametrize( - 'boolean_expr, ladder_target', + 'boolean_str, ladder_target', itertools.product( [ 'x', @@ -64,29 +64,31 @@ def test_unsupported_op(): [False, True], ), ) -def test_circuit(boolean_expr, ladder_target): - # We use Sympy to evaluate the expression: - parsed_expr = sympy_parser.parse_expr(boolean_expr) - var_names = hr.get_name_to_id([parsed_expr]) +def test_circuit(boolean_str, ladder_target): + boolean_expr = sympy_parser.parse_expr(boolean_str) + var_names = cirq.HamiltonianGate.get_name_to_id([boolean_expr]) + + qubits = [cirq.NamedQubit(name) for name in var_names] + # We use Sympy to evaluate the expression: n = len(var_names) expected = [] for binary_inputs in itertools.product([0, 1], repeat=n): - subed_expr = parsed_expr + subed_expr = boolean_expr for var_name, binary_input in zip(var_names, binary_inputs): subed_expr = subed_expr.subs(var_name, binary_input) expected.append(bool(subed_expr)) # We build a circuit and look at its output state vector: - circuit_hamiltonians, qubits = hr.build_circuit_from_boolean_expressions( - [boolean_expr], 0.1 * math.pi, ladder_target - ) + circuit = cirq.Circuit() + circuit.append(cirq.H.on_each(*qubits)) + + hamiltonian_gate = cirq.HamiltonianGate([boolean_expr], 0.1 * math.pi, ladder_target) - circuit_hadamard = cirq.Circuit() - circuit_hadamard.append(cirq.H.on_each(*qubits)) + assert hamiltonian_gate.num_qubits() == n - circuit = circuit_hadamard + circuit_hamiltonians + circuit.append(cirq.decompose(hamiltonian_gate(*qubits))) phi = cirq.Simulator().simulate(circuit, qubit_order=qubits, initial_state=0).state_vector() actual = np.arctan2(phi.real, phi.imag) - math.pi / 2.0 > 0.0 @@ -115,7 +117,7 @@ def test_gray_code_sorting(n_bits, expected_hs): hs.append(tuple(sorted(h))) random.shuffle(hs) - sorted_hs = sorted(list(hs), key=functools.cmp_to_key(hr._gray_code_comparator)) + sorted_hs = sorted(list(hs), key=functools.cmp_to_key(hg._gray_code_comparator)) np.testing.assert_array_equal(sorted_hs, expected_hs) @@ -130,4 +132,4 @@ def test_gray_code_sorting(n_bits, expected_hs): ], ) def test_gray_code_comparison(seq_a, seq_b, expected): - assert hr._gray_code_comparator(seq_a, seq_b) == expected + assert hg._gray_code_comparator(seq_a, seq_b) == expected From a3d7291315ad7b94e0ff2a18343ea14cc33232a7 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 15 May 2021 07:15:16 +0000 Subject: [PATCH 41/87] Add gate --- cirq-core/cirq/json_resolver_cache.py | 1 + cirq-core/cirq/ops/hamiltonian_gate.py | 37 ++-- cirq-core/cirq/ops/hamiltonian_gate_test.py | 2 +- .../json_test_data/HamiltonianGate.json | 10 + .../json_test_data/HamiltonianGate.repr | 1 + examples/examples_test.py | 2 +- examples/qaoa.py | 171 +++++------------- 7 files changed, 85 insertions(+), 139 deletions(-) create mode 100644 cirq-core/cirq/protocols/json_test_data/HamiltonianGate.json create mode 100644 cirq-core/cirq/protocols/json_test_data/HamiltonianGate.repr diff --git a/cirq-core/cirq/json_resolver_cache.py b/cirq-core/cirq/json_resolver_cache.py index 1dbadb5acb2..823397e1888 100644 --- a/cirq-core/cirq/json_resolver_cache.py +++ b/cirq-core/cirq/json_resolver_cache.py @@ -84,6 +84,7 @@ def two_qubit_matrix_gate(matrix): 'GridParallelXEBMetadata': GridParallelXEBMetadata, 'GridQid': cirq.GridQid, 'GridQubit': cirq.GridQubit, + 'HamiltonianGate': cirq.HamiltonianGate, 'HPowGate': cirq.HPowGate, 'ISwapPowGate': cirq.ISwapPowGate, 'IdentityGate': cirq.IdentityGate, diff --git a/cirq-core/cirq/ops/hamiltonian_gate.py b/cirq-core/cirq/ops/hamiltonian_gate.py index 73d34b4ddb2..9b1133906f2 100644 --- a/cirq-core/cirq/ops/hamiltonian_gate.py +++ b/cirq-core/cirq/ops/hamiltonian_gate.py @@ -1,11 +1,12 @@ from collections import defaultdict import functools import math -from typing import DefaultDict, Dict, List, Sequence, Tuple +from typing import Any, DefaultDict, Dict, List, Sequence, Tuple from sympy.logic.boolalg import And, Not, Or, Xor from sympy.core.expr import Expr from sympy.core.symbol import Symbol +import sympy.parsing.sympy_parser as sympy_parser import cirq from cirq import value @@ -142,7 +143,7 @@ def _gray_code_comparator(k1, k2, flip=False): return _gray_code_comparator(k1[0:-1], k2[0:-1], not flip) -def get_gates_from_hamiltonians( +def _get_gates_from_hamiltonians( hamiltonian_polynomial_list: List[HamiltonianPolynomial], qubits, theta: float, @@ -291,27 +292,29 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): class HamiltonianGate(raw_types.Gate): """A gate that applies an Hamiltonian from a set of Boolean functions.""" - def __init__(self, boolean_exprs: Sequence[Expr], theta: float, ladder_target: bool): + def __init__(self, boolean_strs: Sequence[str], theta: float, ladder_target: bool): """ Builds an HamiltonianGate. Args: - boolean_exprs: The list of Sympy Boolean expressions. + boolean_strs: The list of Sympy-parsable Boolean expressions. theta: The list of thetas to scale the Hamiltonian. ladder_target: Whether to use convention of figure 7a or 7b. """ - self._boolean_exprs: Sequence[Expr] = boolean_exprs + self._boolean_strs: Sequence[str] = boolean_strs self._theta: float = theta self._ladder_target: bool = ladder_target - self._name_to_id = HamiltonianGate.get_name_to_id(boolean_exprs) + boolean_exprs = [sympy_parser.parse_expr(boolean_str) for boolean_str in boolean_strs] + name_to_id = HamiltonianGate.get_name_to_id(boolean_exprs) self._hamiltonian_polynomial_list = [ - _build_hamiltonian_from_boolean(boolean, self._name_to_id) - for boolean in self._boolean_exprs + _build_hamiltonian_from_boolean(boolean_expr, name_to_id) + for boolean_expr in boolean_exprs ] + self._num_qubits = len(name_to_id) def num_qubits(self) -> int: - return len(self._name_to_id) + return self._num_qubits @staticmethod def get_name_to_id(boolean_exprs: Sequence[Expr]) -> Dict[str, int]: @@ -331,9 +334,21 @@ def get_name_to_id(boolean_exprs: Sequence[Expr]) -> Dict[str, int]: return {symbol_name: i for i, symbol_name in enumerate(symbol_names)} def _value_equality_values_(self): - return self._boolean_exprs, self._theta, self._ladder_target + return self._boolean_strs, self._theta, self._ladder_target + + def _json_dict_(self) -> Dict[str, Any]: + return { + 'cirq_type': self.__class__.__name__, + 'boolean_strs': self._boolean_strs, + 'theta': self._theta, + 'ladder_target': self._ladder_target, + } + + @classmethod + def _from_json_dict_(cls, boolean_strs, theta, ladder_target, **kwargs): + return cls(boolean_strs, theta, ladder_target) def _decompose_(self, qubits): - yield get_gates_from_hamiltonians( + yield _get_gates_from_hamiltonians( self._hamiltonian_polynomial_list, qubits, self._theta, self._ladder_target ) diff --git a/cirq-core/cirq/ops/hamiltonian_gate_test.py b/cirq-core/cirq/ops/hamiltonian_gate_test.py index 63c2d6a1cad..ff5b2a31ddc 100644 --- a/cirq-core/cirq/ops/hamiltonian_gate_test.py +++ b/cirq-core/cirq/ops/hamiltonian_gate_test.py @@ -84,7 +84,7 @@ def test_circuit(boolean_str, ladder_target): circuit = cirq.Circuit() circuit.append(cirq.H.on_each(*qubits)) - hamiltonian_gate = cirq.HamiltonianGate([boolean_expr], 0.1 * math.pi, ladder_target) + hamiltonian_gate = cirq.HamiltonianGate([boolean_str], 0.1 * math.pi, ladder_target) assert hamiltonian_gate.num_qubits() == n diff --git a/cirq-core/cirq/protocols/json_test_data/HamiltonianGate.json b/cirq-core/cirq/protocols/json_test_data/HamiltonianGate.json new file mode 100644 index 00000000000..6818343cf37 --- /dev/null +++ b/cirq-core/cirq/protocols/json_test_data/HamiltonianGate.json @@ -0,0 +1,10 @@ +[ + { + "cirq_type": "HamiltonianGate", + "boolean_strs": [ + "q0" + ], + "theta": 0.20160913, + "ladder_target": false + } +] \ No newline at end of file diff --git a/cirq-core/cirq/protocols/json_test_data/HamiltonianGate.repr b/cirq-core/cirq/protocols/json_test_data/HamiltonianGate.repr new file mode 100644 index 00000000000..a4881c25b2a --- /dev/null +++ b/cirq-core/cirq/protocols/json_test_data/HamiltonianGate.repr @@ -0,0 +1 @@ +[cirq.HamiltonianGate(['q0'], 0.20160913, False)] \ No newline at end of file diff --git a/examples/examples_test.py b/examples/examples_test.py index 0ffc2e57e16..d3e77f99f38 100644 --- a/examples/examples_test.py +++ b/examples/examples_test.py @@ -97,7 +97,7 @@ def test_example_heatmaps(): def test_example_runs_qaoa(): - examples.qaoa.main(repetitions=10, maxiter=5) + examples.qaoa.main(repetitions=1, maxiter=1, p=1) def test_example_runs_quantum_teleportation(): diff --git a/examples/qaoa.py b/examples/qaoa.py index 87528dc25ac..21b1e54c9bf 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -1,155 +1,74 @@ -"""Runs the Quantum Approximate Optimization Algorithm on Max-Cut. - -=== EXAMPLE OUTPUT === - -Example QAOA circuit: - 0 1 2 3 4 5 - │ │ │ │ │ │ - H H H H H H - │ │ │ │ │ │ - ZZ──────────ZZ^(-4/13) │ │ │ │ -┌ │ │ │ │ │ │ ┐ -│ ZZ──────────┼───────────ZZ^(-4/13) │ │ │ │ -│ │ ZZ──────────┼───────────ZZ^(-4/13) │ │ │ -└ │ │ │ │ │ │ ┘ -┌ │ │ │ │ │ │ ┐ -│ ZZ──────────┼───────────┼───────────┼───────────ZZ^(-4/13) │ │ -│ │ ZZ──────────┼───────────┼───────────┼───────────ZZ^(-4/13) │ -└ │ │ │ │ │ │ ┘ - Rx(0.151π) Rx(0.151π) ZZ──────────┼───────────ZZ^(-4/13) │ - │ │ │ │ │ │ - ZZ──────────ZZ^-0.941 ZZ──────────┼───────────┼───────────ZZ^(-4/13) - │ │ │ ZZ──────────ZZ^(-4/13) │ -┌ │ │ │ │ │ │ ┐ -│ │ │ Rx(0.151π) ZZ──────────┼───────────ZZ^(-4/13) │ -│ │ │ │ │ Rx(0.151π) │ │ -└ │ │ │ │ │ │ ┘ - ZZ──────────┼───────────ZZ^-0.941 Rx(0.151π) │ Rx(0.151π) -┌ │ │ │ │ │ │ ┐ -│ ZZ──────────┼───────────┼───────────┼───────────ZZ^-0.941 │ │ -│ │ ZZ──────────┼───────────ZZ^-0.941 │ │ │ -└ │ │ │ │ │ │ ┘ - Rx(-0.448π) ZZ──────────┼───────────┼───────────┼───────────ZZ^-0.941 - │ │ ZZ──────────┼───────────ZZ^-0.941 │ - │ │ │ │ │ │ - │ Rx(-0.448π) ZZ──────────┼───────────┼───────────ZZ^-0.941 - │ │ │ ZZ──────────ZZ^-0.941 │ -┌ │ │ │ │ │ │ ┐ -│ │ │ Rx(-0.448π) ZZ──────────┼───────────ZZ^-0.941 │ -│ │ │ │ │ Rx(-0.448π) │ │ -└ │ │ │ │ │ │ ┘ - │ │ │ Rx(-0.448π) │ Rx(-0.448π) - │ │ │ │ │ │ - M('m')──────M───────────M───────────M───────────M───────────M - │ │ │ │ │ │ -Optimizing objective function ... -The largest cut value found was 7. -The largest possible cut has size 7. -The approximation ratio achieved is 1.0. -""" - +"""Runs the Quantum Approximate Optimization Algorithm on Max-Cut.""" import itertools import numpy as np import networkx import scipy.optimize +from sympy.parsing.sympy_parser import parse_expr import cirq -def main(repetitions=1000, maxiter=50): - # Set problem parameters - n = 6 - p = 2 - - # Generate a random 3-regular graph on n nodes - graph = networkx.random_regular_graph(3, n) +def brute_force(graph, n): + bitstrings = np.array(list(itertools.product(range(2), repeat=n))) + mat = networkx.adjacency_matrix(graph, nodelist=sorted(graph.nodes)) + vecs = (-1) ** bitstrings + vals = 0.5 * np.sum(vecs * (mat @ vecs.T).T, axis=-1) + vals = 0.5 * (graph.size() - vals) + return max(np.round(vals)) - # Make qubits - qubits = cirq.LineQubit.range(n) - # Print an example circuit - betas = np.random.uniform(-np.pi, np.pi, size=p) - gammas = np.random.uniform(-np.pi, np.pi, size=p) - circuit = qaoa_max_cut_circuit(qubits, betas, gammas, graph) - print('Example QAOA circuit:') - print(circuit.to_text_diagram(transpose=True)) +def qaoa(booleans, repetitions, maxiter, p): + boolean_exprs = [parse_expr(boolean) for boolean in booleans] + name_to_id = cirq.HamiltonianGate.get_name_to_id(boolean_exprs) + qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] - # Create variables to store the largest cut and cut value found - largest_cut_found = None - largest_cut_value_found = 0 + def f(x): + # Build the circuit. + circuit = cirq.Circuit() + circuit.append(cirq.H.on_each(*qubits)) - # Initialize simulator - simulator = cirq.Simulator() + for i in range(p): + hamiltonian_gate = cirq.HamiltonianGate(booleans, 2.0 * x[p + i], ladder_target=True) + circuit.append(cirq.decompose(hamiltonian_gate(*qubits))) + circuit.append(cirq.rx(2.0 * x[i]).on_each(*qubits)) - # Define objective function (we'll use the negative expected cut value) + circuit.append(cirq.measure(*qubits, key='m')) - def f(x): - # Create circuit - betas = x[:p] - gammas = x[p:] - circuit = qaoa_max_cut_circuit(qubits, betas, gammas, graph) - # Sample bitstrings from circuit - result = simulator.run(circuit, repetitions=repetitions) + # Measure + result = cirq.Simulator().run(circuit, repetitions=repetitions) bitstrings = result.measurements['m'] - # Process bitstrings - nonlocal largest_cut_found - nonlocal largest_cut_value_found - values = cut_values(bitstrings, graph) - max_value_index = np.argmax(values) - max_value = values[max_value_index] - if max_value > largest_cut_value_found: - largest_cut_value_found = max_value - largest_cut_found = bitstrings[max_value_index] - mean = np.mean(values) - return -mean - - # Pick an initial guess - x0 = np.random.uniform(-np.pi, np.pi, size=2 * p) - - # Optimize f - print('Optimizing objective function ...') - scipy.optimize.minimize(f, x0, method='Nelder-Mead', options={'maxiter': maxiter}) - # Compute best possible cut value via brute force search - all_bitstrings = np.array(list(itertools.product(range(2), repeat=n))) - all_values = cut_values(all_bitstrings, graph) - max_cut_value = np.max(all_values) + # Evaluate + values = [] + for rep in range(repetitions): + subs = {name: val == 1 for name, val in zip(name_to_id.keys(), bitstrings[rep, :])} + values.append( + sum(1 if boolean_expr.subs(subs) else 0 for boolean_expr in boolean_exprs) + ) - # Print the results - print(f'The largest cut value found was {largest_cut_value_found}.') - print(f'The largest possible cut has size {max_cut_value}.') - print(f'The approximation ratio achieved is {largest_cut_value_found / max_cut_value}.') + print('μ=%.2f max=%d' % (np.mean(values), max(values))) + return -np.mean(values) -def rzz(rads): - """Returns a gate with the matrix exp(-i Z⊗Z rads).""" - return cirq.ZZPowGate(exponent=2 * rads / np.pi, global_shift=-0.5) + x0 = np.zeros(2 * p) + scipy.optimize.minimize(f, x0, method='COBYLA', options={'maxiter': maxiter, 'disp': True}) -def qaoa_max_cut_unitary(qubits, betas, gammas, graph): # Nodes should be integers - for beta, gamma in zip(betas, gammas): - yield (rzz(-0.5 * gamma).on(qubits[i], qubits[j]) for i, j in graph.edges) - yield cirq.rx(2 * beta).on_each(*qubits) +def main(repetitions=10, maxiter=250, p=5): + # Set problem parameters + n = 6 + # Generate a random bipartite graph. + graph = networkx.complete_multipartite_graph(n, n) -def qaoa_max_cut_circuit(qubits, betas, gammas, graph): # Nodes should be integers - return cirq.Circuit( - # Prepare uniform superposition - cirq.H.on_each(*qubits), - # Apply QAOA unitary - qaoa_max_cut_unitary(qubits, betas, gammas, graph), - # Measure - cirq.measure(*qubits, key='m'), - ) + # Compute best possible cut value via brute force search + print('Brute force max cut: %d' % (brute_force(graph, 2 * n))) + # Build the boolean expressions + booleans = [f"x{i} ^ x{j}" for i, j in graph.edges] -def cut_values(bitstrings, graph): - mat = networkx.adjacency_matrix(graph, nodelist=sorted(graph.nodes)) - vecs = (-1) ** bitstrings - vals = 0.5 * np.sum(vecs * (mat @ vecs.T).T, axis=-1) - vals = 0.5 * (graph.size() - vals) - return vals + qaoa(booleans, repetitions=repetitions, maxiter=maxiter, p=p) if __name__ == '__main__': From 4a994c4e9521132f5ebc9a2479f522007d74514a Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Thu, 20 May 2021 06:39:55 +0000 Subject: [PATCH 42/87] Address some comments --- cirq-core/cirq/ops/hamiltonian_gate.py | 54 ++++++++++++++++++--- cirq-core/cirq/ops/hamiltonian_gate_test.py | 12 ++--- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/cirq-core/cirq/ops/hamiltonian_gate.py b/cirq-core/cirq/ops/hamiltonian_gate.py index 9b1133906f2..4c43ec6b2d8 100644 --- a/cirq-core/cirq/ops/hamiltonian_gate.py +++ b/cirq-core/cirq/ops/hamiltonian_gate.py @@ -1,7 +1,7 @@ from collections import defaultdict import functools import math -from typing import Any, DefaultDict, Dict, List, Sequence, Tuple +from typing import Any, DefaultDict, Dict, Iterator, List, Sequence, Tuple from sympy.logic.boolalg import And, Not, Or, Xor from sympy.core.expr import Expr @@ -16,6 +16,15 @@ class HamiltonianPolynomial: """A container class of Boolean function as equation (2) of [1] + It is essentially a polynomial of Pauli Zs on different qubits. For example, this object could + represent the polynomial 0.5*I - 0.5*Z_1*Z_2 and it would be stored inside this object as: + self._hamiltonians = {(): 0.5, (1, 2): 0.5}. + + While this object can represent any polynomial of Pauli Zs, in this file, it will be used to + represent a Boolean operation which has a unique representation as a polynomial. This object + only handle basic operation on the polynomials (e.g. multiplication). The construction of a + polynomial from a Boolean is performed by _build_hamiltonian_from_boolean(). + References: [1] On the representation of Boolean and real functions as Hamiltonians for quantum computing by Stuart Hadfield, https://arxiv.org/pdf/1804.09130.pdf @@ -39,7 +48,7 @@ def hamiltonians(self) -> DefaultDict[Tuple[int, ...], float]: def __repr__(self): # For run-to-run identicalness, we sort the keys lexicographically. formatted_terms = [ - f"{self._hamiltonians[h]:.2f}.{'.'.join('Z_%d' % d for d in h) if h else 'I'}" + f"{self._hamiltonians[h]:.2f}*{'*'.join('Z_%d' % d for d in h) if h else 'I'}" for h in sorted(self._hamiltonians) ] return "; ".join(formatted_terms) @@ -133,7 +142,19 @@ def _build_hamiltonian_from_boolean( raise ValueError(f'Unsupported type: {type(boolean_expr)}') -def _gray_code_comparator(k1, k2, flip=False): +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: @@ -145,10 +166,10 @@ def _gray_code_comparator(k1, k2, flip=False): def _get_gates_from_hamiltonians( hamiltonian_polynomial_list: List[HamiltonianPolynomial], - qubits, + qubits: Sequence['cirq.Qid'], theta: float, ladder_target: bool = False, -): +) -> Iterator['cirq.ops.gate_operation.GateOperation']: """Builds a circuit according to [1]. Args: @@ -254,7 +275,7 @@ def _simplify_cnots(cnots): return cnots - def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): + def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]) -> Iterator['cirq.ops.gate_operation.GateOperation']: # This function applies in sequence the CNOTs from prevh and then currh. However, given # that the h are sorted in Gray ordering and that some cancel each other, we can reduce # the number of gates. See [4] for more details. @@ -290,12 +311,25 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): @value.value_equality class HamiltonianGate(raw_types.Gate): - """A gate that applies an Hamiltonian from a set of Boolean functions.""" + """A gate that applies a Hamiltonian from a set of Boolean functions.""" def __init__(self, boolean_strs: Sequence[str], theta: float, ladder_target: bool): """ Builds an HamiltonianGate. + For each element of a sequence of Boolean expressions, the code first transforms it into a + polynomial of Pauli Zs that represent that particular expression. Then, we sum all the + polynomials, thus making a function that goes from a series to Boolean inputs to an integer + that is the number of Boolean expressions that are true. + + For example, if we were using this gate for the max-cut problem that is typically used to + demonstrate the QAOA algorithm, there would be one Boolean expression per edge. Each + Boolean expression would be true iff the verteces on that are in different cuts (i.e. it's) + an XOR. + + Then, we compute exp(j * theta * polynomial), which is unitary because the polynomial is + Hermitian. + Args: boolean_strs: The list of Sympy-parsable Boolean expressions. theta: The list of thetas to scale the Hamiltonian. @@ -304,6 +338,10 @@ def __init__(self, boolean_strs: Sequence[str], theta: float, ladder_target: boo self._boolean_strs: Sequence[str] = boolean_strs self._theta: float = theta self._ladder_target: bool = ladder_target + # TODO(tonybruguier): Add the ability to control qubit ordering. Consider adding a list of + # symbol names (has to contain all the symbols exactly once, which we can validate) in case + # someone wants to remap the terms to different qubits. If it is None, we can revert to the + # lexicographical sorting. boolean_exprs = [sympy_parser.parse_expr(boolean_str) for boolean_str in boolean_strs] name_to_id = HamiltonianGate.get_name_to_id(boolean_exprs) @@ -348,7 +386,7 @@ def _json_dict_(self) -> Dict[str, Any]: def _from_json_dict_(cls, boolean_strs, theta, ladder_target, **kwargs): return cls(boolean_strs, theta, ladder_target) - def _decompose_(self, qubits): + def _decompose_(self, qubits: Sequence['cirq.Qid']): yield _get_gates_from_hamiltonians( self._hamiltonian_polynomial_list, qubits, self._theta, self._ladder_target ) diff --git a/cirq-core/cirq/ops/hamiltonian_gate_test.py b/cirq-core/cirq/ops/hamiltonian_gate_test.py index ff5b2a31ddc..ae3c622b743 100644 --- a/cirq-core/cirq/ops/hamiltonian_gate_test.py +++ b/cirq-core/cirq/ops/hamiltonian_gate_test.py @@ -14,12 +14,12 @@ @pytest.mark.parametrize( 'boolean_expr,expected_hamiltonian_polynomial', [ - ('x', '0.50.I; -0.50.Z_0'), - ('~x', '0.50.I; 0.50.Z_0'), - ('x0 ^ x1', '0.50.I; -0.50.Z_0.Z_1'), - ('x0 & x1', '0.25.I; -0.25.Z_0; 0.25.Z_0.Z_1; -0.25.Z_1'), - ('x0 | x1', '0.75.I; -0.25.Z_0; -0.25.Z_0.Z_1; -0.25.Z_1'), - ('x0 ^ x1 ^ x2', '0.50.I; -0.50.Z_0.Z_1.Z_2'), + ('x', '0.50*I; -0.50*Z_0'), + ('~x', '0.50*I; 0.50*Z_0'), + ('x0 ^ x1', '0.50*I; -0.50*Z_0*Z_1'), + ('x0 & x1', '0.25*I; -0.25*Z_0; 0.25*Z_0*Z_1; -0.25*Z_1'), + ('x0 | x1', '0.75*I; -0.25*Z_0; -0.25*Z_0*Z_1; -0.25*Z_1'), + ('x0 ^ x1 ^ x2', '0.50*I; -0.50*Z_0*Z_1*Z_2'), ], ) def test_build_hamiltonian_from_boolean(boolean_expr, expected_hamiltonian_polynomial): From 1105d2bbd0ba30f90b2199e539af5a9af323e4db Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Thu, 20 May 2021 06:59:43 +0000 Subject: [PATCH 43/87] Rename gate --- cirq-core/cirq/__init__.py | 2 +- cirq-core/cirq/json_resolver_cache.py | 2 +- cirq-core/cirq/ops/__init__.py | 8 ++++---- ...ate.py => boolean_hamiltonian_operation.py} | 14 +++++++------- ...y => boolean_hamiltonian_operation_test.py} | 18 +++++++++--------- ...e.json => BooleanHamiltonianOperation.json} | 2 +- .../BooleanHamiltonianOperation.repr | 1 + .../json_test_data/HamiltonianGate.repr | 1 - examples/qaoa.py | 6 ++++-- 9 files changed, 28 insertions(+), 26 deletions(-) rename cirq-core/cirq/ops/{hamiltonian_gate.py => boolean_hamiltonian_operation.py} (97%) rename cirq-core/cirq/ops/{hamiltonian_gate_test.py => boolean_hamiltonian_operation_test.py} (83%) rename cirq-core/cirq/protocols/json_test_data/{HamiltonianGate.json => BooleanHamiltonianOperation.json} (68%) create mode 100644 cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.repr delete mode 100644 cirq-core/cirq/protocols/json_test_data/HamiltonianGate.repr diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index 54cdbe270d4..d861042d3ad 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -174,6 +174,7 @@ BaseDensePauliString, bit_flip, BitFlipChannel, + BooleanHamiltonianOperation, CCX, CCXPowGate, CCZ, @@ -209,7 +210,6 @@ givens, GlobalPhaseOperation, H, - HamiltonianGate, HPowGate, I, identity_each, diff --git a/cirq-core/cirq/json_resolver_cache.py b/cirq-core/cirq/json_resolver_cache.py index 823397e1888..a72417a696f 100644 --- a/cirq-core/cirq/json_resolver_cache.py +++ b/cirq-core/cirq/json_resolver_cache.py @@ -52,6 +52,7 @@ def two_qubit_matrix_gate(matrix): 'AsymmetricDepolarizingChannel': cirq.AsymmetricDepolarizingChannel, 'BitFlipChannel': cirq.BitFlipChannel, 'BitstringAccumulator': cirq.work.BitstringAccumulator, + 'BooleanHamiltonianOperation': cirq.BooleanHamiltonianOperation, 'ProductState': cirq.ProductState, 'CCNotPowGate': cirq.CCNotPowGate, 'CCXPowGate': cirq.CCXPowGate, @@ -84,7 +85,6 @@ def two_qubit_matrix_gate(matrix): 'GridParallelXEBMetadata': GridParallelXEBMetadata, 'GridQid': cirq.GridQid, 'GridQubit': cirq.GridQubit, - 'HamiltonianGate': cirq.HamiltonianGate, 'HPowGate': cirq.HPowGate, 'ISwapPowGate': cirq.ISwapPowGate, 'IdentityGate': cirq.IdentityGate, diff --git a/cirq-core/cirq/ops/__init__.py b/cirq-core/cirq/ops/__init__.py index bb1d8f891d8..cbbc8009ba8 100644 --- a/cirq-core/cirq/ops/__init__.py +++ b/cirq-core/cirq/ops/__init__.py @@ -18,6 +18,10 @@ ArithmeticOperation, ) +from cirq.ops.boolean_hamiltonian_operation import ( + BooleanHamiltonianOperation, +) + from cirq.ops.clifford_gate import ( PauliTransform, SingleQubitCliffordGate, @@ -105,10 +109,6 @@ GateOperation, ) -from cirq.ops.hamiltonian_gate import ( - HamiltonianGate, -) - from cirq.ops.identity import ( I, identity_each, diff --git a/cirq-core/cirq/ops/hamiltonian_gate.py b/cirq-core/cirq/ops/boolean_hamiltonian_operation.py similarity index 97% rename from cirq-core/cirq/ops/hamiltonian_gate.py rename to cirq-core/cirq/ops/boolean_hamiltonian_operation.py index 4c43ec6b2d8..059a63d3a79 100644 --- a/cirq-core/cirq/ops/hamiltonian_gate.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_operation.py @@ -1,7 +1,7 @@ from collections import defaultdict import functools import math -from typing import Any, DefaultDict, Dict, Iterator, List, Sequence, Tuple +from typing import Any, DefaultDict, Dict, List, Sequence, Tuple from sympy.logic.boolalg import And, Not, Or, Xor from sympy.core.expr import Expr @@ -142,7 +142,7 @@ def _build_hamiltonian_from_boolean( raise ValueError(f'Unsupported type: {type(boolean_expr)}') -def _gray_code_comparator(k1 : Tuple[int, ...], k2 : Tuple[int, ...], flip: bool = False) -> int: +def _gray_code_comparator(k1: Tuple[int, ...], k2: Tuple[int, ...], flip: bool = False) -> int: """Compares two Gray-encoded binary numbers. Args: @@ -169,7 +169,7 @@ def _get_gates_from_hamiltonians( qubits: Sequence['cirq.Qid'], theta: float, ladder_target: bool = False, -) -> Iterator['cirq.ops.gate_operation.GateOperation']: +): """Builds a circuit according to [1]. Args: @@ -275,7 +275,7 @@ def _simplify_cnots(cnots): return cnots - def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]) -> Iterator['cirq.ops.gate_operation.GateOperation']: + def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): # This function applies in sequence the CNOTs from prevh and then currh. However, given # that the h are sorted in Gray ordering and that some cancel each other, we can reduce # the number of gates. See [4] for more details. @@ -310,12 +310,12 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]) -> Iterator['c @value.value_equality -class HamiltonianGate(raw_types.Gate): +class BooleanHamiltonianOperation(raw_types.Gate): """A gate that applies a Hamiltonian from a set of Boolean functions.""" def __init__(self, boolean_strs: Sequence[str], theta: float, ladder_target: bool): """ - Builds an HamiltonianGate. + Builds an BooleanHamiltonianOperation. For each element of a sequence of Boolean expressions, the code first transforms it into a polynomial of Pauli Zs that represent that particular expression. Then, we sum all the @@ -344,7 +344,7 @@ def __init__(self, boolean_strs: Sequence[str], theta: float, ladder_target: boo # lexicographical sorting. boolean_exprs = [sympy_parser.parse_expr(boolean_str) for boolean_str in boolean_strs] - name_to_id = HamiltonianGate.get_name_to_id(boolean_exprs) + name_to_id = BooleanHamiltonianOperation.get_name_to_id(boolean_exprs) self._hamiltonian_polynomial_list = [ _build_hamiltonian_from_boolean(boolean_expr, name_to_id) for boolean_expr in boolean_exprs diff --git a/cirq-core/cirq/ops/hamiltonian_gate_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py similarity index 83% rename from cirq-core/cirq/ops/hamiltonian_gate_test.py rename to cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py index ae3c622b743..294aed44fcc 100644 --- a/cirq-core/cirq/ops/hamiltonian_gate_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py @@ -8,7 +8,7 @@ import sympy.parsing.sympy_parser as sympy_parser import cirq -import cirq.ops.hamiltonian_gate as hg +import cirq.ops.boolean_hamiltonian_operation as bho # These are some of the entries of table 1 of https://arxiv.org/pdf/1804.09130.pdf. @pytest.mark.parametrize( @@ -24,16 +24,16 @@ ) def test_build_hamiltonian_from_boolean(boolean_expr, expected_hamiltonian_polynomial): boolean = sympy_parser.parse_expr(boolean_expr) - name_to_id = cirq.HamiltonianGate.get_name_to_id([boolean]) - actual = hg._build_hamiltonian_from_boolean(boolean, name_to_id) + name_to_id = cirq.BooleanHamiltonianOperation.get_name_to_id([boolean]) + actual = bho._build_hamiltonian_from_boolean(boolean, name_to_id) assert expected_hamiltonian_polynomial == str(actual) def test_unsupported_op(): not_a_boolean = sympy_parser.parse_expr('x * x') - name_to_id = cirq.HamiltonianGate.get_name_to_id([not_a_boolean]) + name_to_id = cirq.BooleanHamiltonianOperation.get_name_to_id([not_a_boolean]) with pytest.raises(ValueError, match='Unsupported type'): - hg._build_hamiltonian_from_boolean(not_a_boolean, name_to_id) + bho._build_hamiltonian_from_boolean(not_a_boolean, name_to_id) @pytest.mark.parametrize( @@ -66,7 +66,7 @@ def test_unsupported_op(): ) def test_circuit(boolean_str, ladder_target): boolean_expr = sympy_parser.parse_expr(boolean_str) - var_names = cirq.HamiltonianGate.get_name_to_id([boolean_expr]) + var_names = cirq.BooleanHamiltonianOperation.get_name_to_id([boolean_expr]) qubits = [cirq.NamedQubit(name) for name in var_names] @@ -84,7 +84,7 @@ def test_circuit(boolean_str, ladder_target): circuit = cirq.Circuit() circuit.append(cirq.H.on_each(*qubits)) - hamiltonian_gate = cirq.HamiltonianGate([boolean_str], 0.1 * math.pi, ladder_target) + hamiltonian_gate = cirq.BooleanHamiltonianOperation([boolean_str], 0.1 * math.pi, ladder_target) assert hamiltonian_gate.num_qubits() == n @@ -117,7 +117,7 @@ def test_gray_code_sorting(n_bits, expected_hs): hs.append(tuple(sorted(h))) random.shuffle(hs) - sorted_hs = sorted(list(hs), key=functools.cmp_to_key(hg._gray_code_comparator)) + sorted_hs = sorted(list(hs), key=functools.cmp_to_key(bho._gray_code_comparator)) np.testing.assert_array_equal(sorted_hs, expected_hs) @@ -132,4 +132,4 @@ def test_gray_code_sorting(n_bits, expected_hs): ], ) def test_gray_code_comparison(seq_a, seq_b, expected): - assert hg._gray_code_comparator(seq_a, seq_b) == expected + assert bho._gray_code_comparator(seq_a, seq_b) == expected diff --git a/cirq-core/cirq/protocols/json_test_data/HamiltonianGate.json b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.json similarity index 68% rename from cirq-core/cirq/protocols/json_test_data/HamiltonianGate.json rename to cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.json index 6818343cf37..07e6b6b816c 100644 --- a/cirq-core/cirq/protocols/json_test_data/HamiltonianGate.json +++ b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.json @@ -1,6 +1,6 @@ [ { - "cirq_type": "HamiltonianGate", + "cirq_type": "BooleanHamiltonianOperation", "boolean_strs": [ "q0" ], diff --git a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.repr b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.repr new file mode 100644 index 00000000000..1f5c9330eff --- /dev/null +++ b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.repr @@ -0,0 +1 @@ +[cirq.BooleanHamiltonianOperation(['q0'], 0.20160913, False)] \ No newline at end of file diff --git a/cirq-core/cirq/protocols/json_test_data/HamiltonianGate.repr b/cirq-core/cirq/protocols/json_test_data/HamiltonianGate.repr deleted file mode 100644 index a4881c25b2a..00000000000 --- a/cirq-core/cirq/protocols/json_test_data/HamiltonianGate.repr +++ /dev/null @@ -1 +0,0 @@ -[cirq.HamiltonianGate(['q0'], 0.20160913, False)] \ No newline at end of file diff --git a/examples/qaoa.py b/examples/qaoa.py index 21b1e54c9bf..c06ae779ef1 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -20,7 +20,7 @@ def brute_force(graph, n): def qaoa(booleans, repetitions, maxiter, p): boolean_exprs = [parse_expr(boolean) for boolean in booleans] - name_to_id = cirq.HamiltonianGate.get_name_to_id(boolean_exprs) + name_to_id = cirq.BooleanHamiltonianOperation.get_name_to_id(boolean_exprs) qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] def f(x): @@ -29,7 +29,9 @@ def f(x): circuit.append(cirq.H.on_each(*qubits)) for i in range(p): - hamiltonian_gate = cirq.HamiltonianGate(booleans, 2.0 * x[p + i], ladder_target=True) + hamiltonian_gate = cirq.BooleanHamiltonianOperation( + booleans, 2.0 * x[p + i], ladder_target=True + ) circuit.append(cirq.decompose(hamiltonian_gate(*qubits))) circuit.append(cirq.rx(2.0 * x[i]).on_each(*qubits)) From 6cd936674084c5381466bbc9111697ea9f1a2579 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 21 May 2021 04:10:46 +0000 Subject: [PATCH 44/87] execute TODO --- .../cirq/ops/boolean_hamiltonian_operation.py | 42 ++++++++++++------- .../ops/boolean_hamiltonian_operation_test.py | 24 +++++++++++ .../BooleanHamiltonianOperation.json | 3 +- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_operation.py b/cirq-core/cirq/ops/boolean_hamiltonian_operation.py index 059a63d3a79..dd458ebdba5 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_operation.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_operation.py @@ -1,7 +1,7 @@ from collections import defaultdict import functools import math -from typing import Any, DefaultDict, Dict, List, Sequence, Tuple +from typing import Any, DefaultDict, Dict, List, Optional, Sequence, Tuple from sympy.logic.boolalg import And, Not, Or, Xor from sympy.core.expr import Expr @@ -313,7 +313,13 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): class BooleanHamiltonianOperation(raw_types.Gate): """A gate that applies a Hamiltonian from a set of Boolean functions.""" - def __init__(self, boolean_strs: Sequence[str], theta: float, ladder_target: bool): + def __init__( + self, + boolean_strs: Sequence[str], + theta: float, + ladder_target: bool, + symbol_names: Optional[Sequence[str]] = None, + ): """ Builds an BooleanHamiltonianOperation. @@ -334,17 +340,15 @@ def __init__(self, boolean_strs: Sequence[str], theta: float, ladder_target: boo boolean_strs: The list of Sympy-parsable Boolean expressions. theta: The list of thetas to scale the Hamiltonian. ladder_target: Whether to use convention of figure 7a or 7b. + symbol_names: A list of symbol names that should be a superset of all the symbols used. """ self._boolean_strs: Sequence[str] = boolean_strs self._theta: float = theta self._ladder_target: bool = ladder_target - # TODO(tonybruguier): Add the ability to control qubit ordering. Consider adding a list of - # symbol names (has to contain all the symbols exactly once, which we can validate) in case - # someone wants to remap the terms to different qubits. If it is None, we can revert to the - # lexicographical sorting. + self._symbol_names: Optional[Sequence[str]] = symbol_names boolean_exprs = [sympy_parser.parse_expr(boolean_str) for boolean_str in boolean_strs] - name_to_id = BooleanHamiltonianOperation.get_name_to_id(boolean_exprs) + name_to_id = BooleanHamiltonianOperation.get_name_to_id(boolean_exprs, symbol_names) self._hamiltonian_polynomial_list = [ _build_hamiltonian_from_boolean(boolean_expr, name_to_id) for boolean_expr in boolean_exprs @@ -355,21 +359,30 @@ def num_qubits(self) -> int: return self._num_qubits @staticmethod - def get_name_to_id(boolean_exprs: Sequence[Expr]) -> Dict[str, int]: + def get_name_to_id( + boolean_exprs: Sequence[Expr], symbol_names: Optional[Sequence[str]] = None + ) -> Dict[str, int]: """Maps the variables to a unique integer. Args: boolean_expr: A Sympy expression containing symbols and Boolean operations + symbol_names: A list of symbol names that should be a superset of all the symbols used. Return: A dictionary of string (the variable name) to a unique integer. """ + required_symbol_names = { + symbol.name for boolean_expr in boolean_exprs for symbol in boolean_expr.free_symbols + } # For run-to-run identicalness, we sort the symbol name lexicographically. - symbol_names = sorted( - {symbol.name for boolean_expr in boolean_exprs for symbol in boolean_expr.free_symbols} - ) - return {symbol_name: i for i, symbol_name in enumerate(symbol_names)} + if symbol_names: + for symb in required_symbol_names: + if symb not in symbol_names: + raise ValueError(f'Missing required symbol: {symb}') + return {symb: i for i, symb in enumerate(sorted(symbol_names))} + else: + return {symb: i for i, symb in enumerate(sorted(required_symbol_names))} def _value_equality_values_(self): return self._boolean_strs, self._theta, self._ladder_target @@ -380,11 +393,12 @@ def _json_dict_(self) -> Dict[str, Any]: 'boolean_strs': self._boolean_strs, 'theta': self._theta, 'ladder_target': self._ladder_target, + 'symbol_names': self._symbol_names, } @classmethod - def _from_json_dict_(cls, boolean_strs, theta, ladder_target, **kwargs): - return cls(boolean_strs, theta, ladder_target) + def _from_json_dict_(cls, boolean_strs, theta, ladder_target, symbol_names, **kwargs): + return cls(boolean_strs, theta, ladder_target, symbol_names) def _decompose_(self, qubits: Sequence['cirq.Qid']): yield _get_gates_from_hamiltonians( diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py index 294aed44fcc..f21e4176a99 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py @@ -36,6 +36,30 @@ def test_unsupported_op(): bho._build_hamiltonian_from_boolean(not_a_boolean, name_to_id) +@pytest.mark.parametrize( + 'boolean_strs,symbol_names,expected', + [ + (['x0'], None, {'x0': 0}), + (['x0 & x1'], None, {'x0': 0, 'x1': 1}), + (['x0', 'x1'], None, {'x0': 0, 'x1': 1}), + (['x1', 'x0'], None, {'x0': 0, 'x1': 1}), + (['x1', 'x0'], ['x2', 'x0', 'x1'], {'x0': 0, 'x1': 1, 'x2': 2}), + ], +) +def test_get_name_to_id(boolean_strs, symbol_names, expected): + assert ( + cirq.BooleanHamiltonianOperation.get_name_to_id( + [sympy_parser.parse_expr(boolean_str) for boolean_str in boolean_strs], symbol_names + ) + == expected + ) + + +def test_get_name_to_id_missing_required_symbol(): + with pytest.raises(ValueError, match='Missing required symbol: x1'): + cirq.BooleanHamiltonianOperation.get_name_to_id([sympy_parser.parse_expr('x1')], ['x2']) + + @pytest.mark.parametrize( 'boolean_str, ladder_target', itertools.product( diff --git a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.json b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.json index 07e6b6b816c..d178c7be7da 100644 --- a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.json +++ b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.json @@ -5,6 +5,7 @@ "q0" ], "theta": 0.20160913, - "ladder_target": false + "ladder_target": false, + "symbol_names": null } ] \ No newline at end of file From a0c700fc68224f67d20ad5549d01db85ddecc5f1 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 21 May 2021 12:08:13 +0000 Subject: [PATCH 45/87] More unit tests --- .../ops/boolean_hamiltonian_operation_test.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py index f21e4176a99..4b99c2852ae 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py @@ -37,7 +37,7 @@ def test_unsupported_op(): @pytest.mark.parametrize( - 'boolean_strs,symbol_names,expected', + 'boolean_strs, symbol_names,expected', [ (['x0'], None, {'x0': 0}), (['x0 & x1'], None, {'x0': 0, 'x1': 1}), @@ -61,11 +61,11 @@ def test_get_name_to_id_missing_required_symbol(): @pytest.mark.parametrize( - 'boolean_str, ladder_target', + 'boolean_str, ladder_target, symbol_names', itertools.product( [ - 'x', - '~x', + 'x0', + '~x0', 'x0 ^ x1', 'x0 & x1', 'x0 | x1', @@ -86,11 +86,12 @@ def test_get_name_to_id_missing_required_symbol(): '(x2 | x1) ^ x0', ], [False, True], + [None, ['x4', 'x0', 'x3', 'x2', 'x1']], ), ) -def test_circuit(boolean_str, ladder_target): +def test_circuit(boolean_str, ladder_target, symbol_names): boolean_expr = sympy_parser.parse_expr(boolean_str) - var_names = cirq.BooleanHamiltonianOperation.get_name_to_id([boolean_expr]) + var_names = cirq.BooleanHamiltonianOperation.get_name_to_id([boolean_expr], symbol_names) qubits = [cirq.NamedQubit(name) for name in var_names] @@ -108,7 +109,9 @@ def test_circuit(boolean_str, ladder_target): circuit = cirq.Circuit() circuit.append(cirq.H.on_each(*qubits)) - hamiltonian_gate = cirq.BooleanHamiltonianOperation([boolean_str], 0.1 * math.pi, ladder_target) + hamiltonian_gate = cirq.BooleanHamiltonianOperation( + [boolean_str], 0.1 * math.pi, ladder_target, symbol_names + ) assert hamiltonian_gate.num_qubits() == n From dacf48fe774735e492282407574bde0ed5245ec8 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sun, 23 May 2021 17:49:39 +0000 Subject: [PATCH 46/87] nits --- cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py index 4b99c2852ae..d7b1c5d7acf 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py @@ -37,7 +37,7 @@ def test_unsupported_op(): @pytest.mark.parametrize( - 'boolean_strs, symbol_names,expected', + 'boolean_strs,symbol_names,expected', [ (['x0'], None, {'x0': 0}), (['x0 & x1'], None, {'x0': 0, 'x1': 1}), @@ -61,7 +61,7 @@ def test_get_name_to_id_missing_required_symbol(): @pytest.mark.parametrize( - 'boolean_str, ladder_target, symbol_names', + 'boolean_str,ladder_target,symbol_names', itertools.product( [ 'x0', @@ -125,7 +125,7 @@ def test_circuit(boolean_str, ladder_target, symbol_names): @pytest.mark.parametrize( - 'n_bits, expected_hs', + 'n_bits,expected_hs', [ (1, [(), (0,)]), (2, [(), (0,), (0, 1), (1,)]), @@ -150,7 +150,7 @@ def test_gray_code_sorting(n_bits, expected_hs): @pytest.mark.parametrize( - 'seq_a, seq_b, expected', + 'seq_a,seq_b,expected', [ ((), (), 0), ((), (0,), -1), From 57c0f7a18c044f3e636f456be05b3610cb5f359e Mon Sep 17 00:00:00 2001 From: "Antoine (Tony) Bruguier" Date: Thu, 3 Jun 2021 22:36:27 -0700 Subject: [PATCH 47/87] Update cirq-core/cirq/ops/boolean_hamiltonian_operation.py Co-authored-by: Balint Pato --- cirq-core/cirq/ops/boolean_hamiltonian_operation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_operation.py b/cirq-core/cirq/ops/boolean_hamiltonian_operation.py index dd458ebdba5..b670ce3a1d3 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_operation.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_operation.py @@ -330,7 +330,7 @@ def __init__( For example, if we were using this gate for the max-cut problem that is typically used to demonstrate the QAOA algorithm, there would be one Boolean expression per edge. Each - Boolean expression would be true iff the verteces on that are in different cuts (i.e. it's) + Boolean expression would be true iff the vertices on that are in different cuts (i.e. it's) an XOR. Then, we compute exp(j * theta * polynomial), which is unitary because the polynomial is From 09c98d3d5032b8cb2384904da88d922a535725a1 Mon Sep 17 00:00:00 2001 From: "Antoine (Tony) Bruguier" Date: Thu, 3 Jun 2021 22:36:41 -0700 Subject: [PATCH 48/87] Update cirq-core/cirq/ops/boolean_hamiltonian_operation.py Co-authored-by: Balint Pato --- cirq-core/cirq/ops/boolean_hamiltonian_operation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_operation.py b/cirq-core/cirq/ops/boolean_hamiltonian_operation.py index b670ce3a1d3..0bb6796498e 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_operation.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_operation.py @@ -34,7 +34,7 @@ class HamiltonianPolynomial: Greenbaum, Sarah Mostame, Alán Aspuru-Guzik, https://arxiv.org/abs/1306.3991 """ - def __init__(self, hamiltonians: DefaultDict[Tuple[int, ...], float]): + def __init__(self, hamiltonians: Dict[Tuple[int, ...], float]): # The representation is Tuple[int, ...] to weights. The tuple contains the integers of # where Z_i is present. For example, Z_0.Z_3 would be (0, 3), and I is the empty tuple. self._hamiltonians: DefaultDict[Tuple[int, ...], float] = defaultdict( From 3a3ad6a0c8bcbbb016ce2ffbacb50e4d16b93579 Mon Sep 17 00:00:00 2001 From: "Antoine (Tony) Bruguier" Date: Thu, 3 Jun 2021 22:36:49 -0700 Subject: [PATCH 49/87] Update cirq-core/cirq/ops/boolean_hamiltonian_operation.py Co-authored-by: Balint Pato --- cirq-core/cirq/ops/boolean_hamiltonian_operation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_operation.py b/cirq-core/cirq/ops/boolean_hamiltonian_operation.py index 0bb6796498e..f012576b0f8 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_operation.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_operation.py @@ -379,7 +379,7 @@ def get_name_to_id( if symbol_names: for symb in required_symbol_names: if symb not in symbol_names: - raise ValueError(f'Missing required symbol: {symb}') + raise ValueError(f'symbol_names was specified but it is not a superset of the symbols in boolean_exprs. Missing required symbol: {symb}') return {symb: i for i, symb in enumerate(sorted(symbol_names))} else: return {symb: i for i, symb in enumerate(sorted(required_symbol_names))} From 47bb1a16b887a2ed9511bb32023a81321a78a48d Mon Sep 17 00:00:00 2001 From: "Antoine (Tony) Bruguier" Date: Thu, 3 Jun 2021 22:37:42 -0700 Subject: [PATCH 50/87] Update cirq-core/cirq/ops/boolean_hamiltonian_operation.py Co-authored-by: Balint Pato --- cirq-core/cirq/ops/boolean_hamiltonian_operation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_operation.py b/cirq-core/cirq/ops/boolean_hamiltonian_operation.py index f012576b0f8..aaae9225ae8 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_operation.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_operation.py @@ -321,7 +321,7 @@ def __init__( symbol_names: Optional[Sequence[str]] = None, ): """ - Builds an BooleanHamiltonianOperation. + Builds a BooleanHamiltonianOperation. For each element of a sequence of Boolean expressions, the code first transforms it into a polynomial of Pauli Zs that represent that particular expression. Then, we sum all the From 4a9cba425bc3f903c73b315dcb240baa4669f2b1 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 4 Jun 2021 06:00:21 +0000 Subject: [PATCH 51/87] nit --- cirq-core/cirq/ops/boolean_hamiltonian_operation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_operation.py b/cirq-core/cirq/ops/boolean_hamiltonian_operation.py index f012576b0f8..e2c120a8d90 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_operation.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_operation.py @@ -379,7 +379,9 @@ def get_name_to_id( if symbol_names: for symb in required_symbol_names: if symb not in symbol_names: - raise ValueError(f'symbol_names was specified but it is not a superset of the symbols in boolean_exprs. Missing required symbol: {symb}') + raise ValueError( + f'symbol_names was specified but it is not a superset of the symbols in boolean_exprs. Missing required symbol: {symb}' + ) return {symb: i for i, symb in enumerate(sorted(symbol_names))} else: return {symb: i for i, symb in enumerate(sorted(required_symbol_names))} From 20b58964fa33d2e8e6f2befaba4bb048fe7a4968 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 4 Jun 2021 06:14:27 +0000 Subject: [PATCH 52/87] Rename BooleanHamiltonianOperation to BooleanHamiltonian --- cirq-core/cirq/__init__.py | 2 +- cirq-core/cirq/json_resolver_cache.py | 2 +- cirq-core/cirq/ops/__init__.py | 4 ++-- ...miltonian_operation.py => boolean_hamiltonian.py} | 6 +++--- ...operation_test.py => boolean_hamiltonian_test.py} | 12 ++++++------ ...ltonianOperation.json => BooleanHamiltonian.json} | 2 +- .../protocols/json_test_data/BooleanHamiltonian.repr | 1 + .../json_test_data/BooleanHamiltonianOperation.repr | 1 - examples/qaoa.py | 4 ++-- 9 files changed, 17 insertions(+), 17 deletions(-) rename cirq-core/cirq/ops/{boolean_hamiltonian_operation.py => boolean_hamiltonian.py} (98%) rename cirq-core/cirq/ops/{boolean_hamiltonian_operation_test.py => boolean_hamiltonian_test.py} (90%) rename cirq-core/cirq/protocols/json_test_data/{BooleanHamiltonianOperation.json => BooleanHamiltonian.json} (73%) create mode 100644 cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr delete mode 100644 cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.repr diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index 234295bf319..b35584aff50 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -175,7 +175,7 @@ BaseDensePauliString, bit_flip, BitFlipChannel, - BooleanHamiltonianOperation, + BooleanHamiltonian, CCX, CCXPowGate, CCZ, diff --git a/cirq-core/cirq/json_resolver_cache.py b/cirq-core/cirq/json_resolver_cache.py index c60116a6d2d..d6b55c1bba6 100644 --- a/cirq-core/cirq/json_resolver_cache.py +++ b/cirq-core/cirq/json_resolver_cache.py @@ -52,7 +52,7 @@ def two_qubit_matrix_gate(matrix): 'AsymmetricDepolarizingChannel': cirq.AsymmetricDepolarizingChannel, 'BitFlipChannel': cirq.BitFlipChannel, 'BitstringAccumulator': cirq.work.BitstringAccumulator, - 'BooleanHamiltonianOperation': cirq.BooleanHamiltonianOperation, + 'BooleanHamiltonian': cirq.BooleanHamiltonian, 'ProductState': cirq.ProductState, 'CCNotPowGate': cirq.CCNotPowGate, 'CCXPowGate': cirq.CCXPowGate, diff --git a/cirq-core/cirq/ops/__init__.py b/cirq-core/cirq/ops/__init__.py index cbbc8009ba8..177f8f8e327 100644 --- a/cirq-core/cirq/ops/__init__.py +++ b/cirq-core/cirq/ops/__init__.py @@ -18,8 +18,8 @@ ArithmeticOperation, ) -from cirq.ops.boolean_hamiltonian_operation import ( - BooleanHamiltonianOperation, +from cirq.ops.boolean_hamiltonian import ( + BooleanHamiltonian, ) from cirq.ops.clifford_gate import ( diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_operation.py b/cirq-core/cirq/ops/boolean_hamiltonian.py similarity index 98% rename from cirq-core/cirq/ops/boolean_hamiltonian_operation.py rename to cirq-core/cirq/ops/boolean_hamiltonian.py index e2c120a8d90..0581d25a48b 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_operation.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -310,7 +310,7 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): @value.value_equality -class BooleanHamiltonianOperation(raw_types.Gate): +class BooleanHamiltonian(raw_types.Gate): """A gate that applies a Hamiltonian from a set of Boolean functions.""" def __init__( @@ -321,7 +321,7 @@ def __init__( symbol_names: Optional[Sequence[str]] = None, ): """ - Builds an BooleanHamiltonianOperation. + Builds an BooleanHamiltonian. For each element of a sequence of Boolean expressions, the code first transforms it into a polynomial of Pauli Zs that represent that particular expression. Then, we sum all the @@ -348,7 +348,7 @@ def __init__( self._symbol_names: Optional[Sequence[str]] = symbol_names boolean_exprs = [sympy_parser.parse_expr(boolean_str) for boolean_str in boolean_strs] - name_to_id = BooleanHamiltonianOperation.get_name_to_id(boolean_exprs, symbol_names) + name_to_id = BooleanHamiltonian.get_name_to_id(boolean_exprs, symbol_names) self._hamiltonian_polynomial_list = [ _build_hamiltonian_from_boolean(boolean_expr, name_to_id) for boolean_expr in boolean_exprs diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py similarity index 90% rename from cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py rename to cirq-core/cirq/ops/boolean_hamiltonian_test.py index d7b1c5d7acf..8ffc2334fcd 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_operation_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -24,14 +24,14 @@ ) def test_build_hamiltonian_from_boolean(boolean_expr, expected_hamiltonian_polynomial): boolean = sympy_parser.parse_expr(boolean_expr) - name_to_id = cirq.BooleanHamiltonianOperation.get_name_to_id([boolean]) + name_to_id = cirq.BooleanHamiltonian.get_name_to_id([boolean]) actual = bho._build_hamiltonian_from_boolean(boolean, name_to_id) assert expected_hamiltonian_polynomial == str(actual) def test_unsupported_op(): not_a_boolean = sympy_parser.parse_expr('x * x') - name_to_id = cirq.BooleanHamiltonianOperation.get_name_to_id([not_a_boolean]) + name_to_id = cirq.BooleanHamiltonian.get_name_to_id([not_a_boolean]) with pytest.raises(ValueError, match='Unsupported type'): bho._build_hamiltonian_from_boolean(not_a_boolean, name_to_id) @@ -48,7 +48,7 @@ def test_unsupported_op(): ) def test_get_name_to_id(boolean_strs, symbol_names, expected): assert ( - cirq.BooleanHamiltonianOperation.get_name_to_id( + cirq.BooleanHamiltonian.get_name_to_id( [sympy_parser.parse_expr(boolean_str) for boolean_str in boolean_strs], symbol_names ) == expected @@ -57,7 +57,7 @@ def test_get_name_to_id(boolean_strs, symbol_names, expected): def test_get_name_to_id_missing_required_symbol(): with pytest.raises(ValueError, match='Missing required symbol: x1'): - cirq.BooleanHamiltonianOperation.get_name_to_id([sympy_parser.parse_expr('x1')], ['x2']) + cirq.BooleanHamiltonian.get_name_to_id([sympy_parser.parse_expr('x1')], ['x2']) @pytest.mark.parametrize( @@ -91,7 +91,7 @@ def test_get_name_to_id_missing_required_symbol(): ) def test_circuit(boolean_str, ladder_target, symbol_names): boolean_expr = sympy_parser.parse_expr(boolean_str) - var_names = cirq.BooleanHamiltonianOperation.get_name_to_id([boolean_expr], symbol_names) + var_names = cirq.BooleanHamiltonian.get_name_to_id([boolean_expr], symbol_names) qubits = [cirq.NamedQubit(name) for name in var_names] @@ -109,7 +109,7 @@ def test_circuit(boolean_str, ladder_target, symbol_names): circuit = cirq.Circuit() circuit.append(cirq.H.on_each(*qubits)) - hamiltonian_gate = cirq.BooleanHamiltonianOperation( + hamiltonian_gate = cirq.BooleanHamiltonian( [boolean_str], 0.1 * math.pi, ladder_target, symbol_names ) diff --git a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.json b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json similarity index 73% rename from cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.json rename to cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json index d178c7be7da..5ace03045f7 100644 --- a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.json +++ b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json @@ -1,6 +1,6 @@ [ { - "cirq_type": "BooleanHamiltonianOperation", + "cirq_type": "BooleanHamiltonian", "boolean_strs": [ "q0" ], diff --git a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr new file mode 100644 index 00000000000..7b90f08e5af --- /dev/null +++ b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr @@ -0,0 +1 @@ +[cirq.BooleanHamiltonian(['q0'], 0.20160913, False)] \ No newline at end of file diff --git a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.repr b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.repr deleted file mode 100644 index 1f5c9330eff..00000000000 --- a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianOperation.repr +++ /dev/null @@ -1 +0,0 @@ -[cirq.BooleanHamiltonianOperation(['q0'], 0.20160913, False)] \ No newline at end of file diff --git a/examples/qaoa.py b/examples/qaoa.py index c06ae779ef1..69b7bd00866 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -20,7 +20,7 @@ def brute_force(graph, n): def qaoa(booleans, repetitions, maxiter, p): boolean_exprs = [parse_expr(boolean) for boolean in booleans] - name_to_id = cirq.BooleanHamiltonianOperation.get_name_to_id(boolean_exprs) + name_to_id = cirq.BooleanHamiltonian.get_name_to_id(boolean_exprs) qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] def f(x): @@ -29,7 +29,7 @@ def f(x): circuit.append(cirq.H.on_each(*qubits)) for i in range(p): - hamiltonian_gate = cirq.BooleanHamiltonianOperation( + hamiltonian_gate = cirq.BooleanHamiltonian( booleans, 2.0 * x[p + i], ladder_target=True ) circuit.append(cirq.decompose(hamiltonian_gate(*qubits))) From 789d65658435a52db9fcdc2e33943c65822d04a1 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 4 Jun 2021 06:48:42 +0000 Subject: [PATCH 53/87] fix some errors, hopefully --- cirq-core/cirq/ops/boolean_hamiltonian_test.py | 10 +++++----- examples/qaoa.py | 4 +--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index 8ffc2334fcd..bffbac763d0 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -8,7 +8,7 @@ import sympy.parsing.sympy_parser as sympy_parser import cirq -import cirq.ops.boolean_hamiltonian_operation as bho +import cirq.ops.boolean_hamiltonian as bho # These are some of the entries of table 1 of https://arxiv.org/pdf/1804.09130.pdf. @pytest.mark.parametrize( @@ -25,7 +25,7 @@ def test_build_hamiltonian_from_boolean(boolean_expr, expected_hamiltonian_polynomial): boolean = sympy_parser.parse_expr(boolean_expr) name_to_id = cirq.BooleanHamiltonian.get_name_to_id([boolean]) - actual = bho._build_hamiltonian_from_boolean(boolean, name_to_id) + actual = bh._build_hamiltonian_from_boolean(boolean, name_to_id) assert expected_hamiltonian_polynomial == str(actual) @@ -33,7 +33,7 @@ def test_unsupported_op(): not_a_boolean = sympy_parser.parse_expr('x * x') name_to_id = cirq.BooleanHamiltonian.get_name_to_id([not_a_boolean]) with pytest.raises(ValueError, match='Unsupported type'): - bho._build_hamiltonian_from_boolean(not_a_boolean, name_to_id) + bh._build_hamiltonian_from_boolean(not_a_boolean, name_to_id) @pytest.mark.parametrize( @@ -144,7 +144,7 @@ def test_gray_code_sorting(n_bits, expected_hs): hs.append(tuple(sorted(h))) random.shuffle(hs) - sorted_hs = sorted(list(hs), key=functools.cmp_to_key(bho._gray_code_comparator)) + sorted_hs = sorted(list(hs), key=functools.cmp_to_key(bh._gray_code_comparator)) np.testing.assert_array_equal(sorted_hs, expected_hs) @@ -159,4 +159,4 @@ def test_gray_code_sorting(n_bits, expected_hs): ], ) def test_gray_code_comparison(seq_a, seq_b, expected): - assert bho._gray_code_comparator(seq_a, seq_b) == expected + assert bh._gray_code_comparator(seq_a, seq_b) == expected diff --git a/examples/qaoa.py b/examples/qaoa.py index 69b7bd00866..0b6a048875e 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -29,9 +29,7 @@ def f(x): circuit.append(cirq.H.on_each(*qubits)) for i in range(p): - hamiltonian_gate = cirq.BooleanHamiltonian( - booleans, 2.0 * x[p + i], ladder_target=True - ) + hamiltonian_gate = cirq.BooleanHamiltonian(booleans, 2.0 * x[p + i], ladder_target=True) circuit.append(cirq.decompose(hamiltonian_gate(*qubits))) circuit.append(cirq.rx(2.0 * x[i]).on_each(*qubits)) From 83a2fffadf461ff8bf3fe803312aaedbde319837 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 4 Jun 2021 06:52:25 +0000 Subject: [PATCH 54/87] fix import --- cirq-core/cirq/ops/boolean_hamiltonian_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index bffbac763d0..ae79e7a4704 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -8,7 +8,7 @@ import sympy.parsing.sympy_parser as sympy_parser import cirq -import cirq.ops.boolean_hamiltonian as bho +import cirq.ops.boolean_hamiltonian as bh # These are some of the entries of table 1 of https://arxiv.org/pdf/1804.09130.pdf. @pytest.mark.parametrize( From c513e479f43cd82ed205f52bb521e080441b07c2 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 5 Jun 2021 04:35:05 +0000 Subject: [PATCH 55/87] New base class --- cirq-core/cirq/ops/boolean_hamiltonian.py | 32 +++++++++++++++---- .../cirq/ops/boolean_hamiltonian_test.py | 4 +-- .../json_test_data/BooleanHamiltonian.json | 6 ++++ .../json_test_data/BooleanHamiltonian.repr | 2 +- examples/qaoa.py | 6 ++-- 5 files changed, 38 insertions(+), 12 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 0581d25a48b..ccab16dcf64 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -310,11 +310,12 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): @value.value_equality -class BooleanHamiltonian(raw_types.Gate): +class BooleanHamiltonian(raw_types.Operation): """A gate that applies a Hamiltonian from a set of Boolean functions.""" def __init__( self, + qubits: Sequence['cirq.Qid'], boolean_strs: Sequence[str], theta: float, ladder_target: bool, @@ -338,10 +339,12 @@ def __init__( Args: boolean_strs: The list of Sympy-parsable Boolean expressions. + qubits: DO NOT SUBMIT theta: The list of thetas to scale the Hamiltonian. ladder_target: Whether to use convention of figure 7a or 7b. symbol_names: A list of symbol names that should be a superset of all the symbols used. """ + self._qubits: Sequence['cirq.Qid'] = qubits self._boolean_strs: Sequence[str] = boolean_strs self._theta: float = theta self._ladder_target: bool = ladder_target @@ -355,6 +358,19 @@ def __init__( ] self._num_qubits = len(name_to_id) + def with_qubits(self, *new_qubits: 'cirq.Qid') -> 'BooleanHamiltonian': + return BooleanHamiltonian( + list(new_qubits), + self._boolean_strs, + self._theta, + self._ladder_target, + self._symbol_names, + ) + + @property + def qubits(self) -> Tuple[raw_types.Qid, ...]: + return tuple(self._qubits) + def num_qubits(self) -> int: return self._num_qubits @@ -380,18 +396,20 @@ def get_name_to_id( for symb in required_symbol_names: if symb not in symbol_names: raise ValueError( - f'symbol_names was specified but it is not a superset of the symbols in boolean_exprs. Missing required symbol: {symb}' + 'symbol_names was specified but it is not a superset of the symbols ' + + f'in boolean_exprs. Missing required symbol: {symb}' ) return {symb: i for i, symb in enumerate(sorted(symbol_names))} else: return {symb: i for i, symb in enumerate(sorted(required_symbol_names))} def _value_equality_values_(self): - return self._boolean_strs, self._theta, self._ladder_target + return self._qubits, self._boolean_strs, self._theta, self._ladder_target def _json_dict_(self) -> Dict[str, Any]: return { 'cirq_type': self.__class__.__name__, + 'qubits': self._qubits, 'boolean_strs': self._boolean_strs, 'theta': self._theta, 'ladder_target': self._ladder_target, @@ -399,10 +417,10 @@ def _json_dict_(self) -> Dict[str, Any]: } @classmethod - def _from_json_dict_(cls, boolean_strs, theta, ladder_target, symbol_names, **kwargs): - return cls(boolean_strs, theta, ladder_target, symbol_names) + def _from_json_dict_(cls, qubits, boolean_strs, theta, ladder_target, symbol_names, **kwargs): + return cls(qubits, boolean_strs, theta, ladder_target, symbol_names) - def _decompose_(self, qubits: Sequence['cirq.Qid']): + def _decompose_(self): yield _get_gates_from_hamiltonians( - self._hamiltonian_polynomial_list, qubits, self._theta, self._ladder_target + self._hamiltonian_polynomial_list, self._qubits, self._theta, self._ladder_target ) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index ae79e7a4704..2f5290adcf3 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -110,12 +110,12 @@ def test_circuit(boolean_str, ladder_target, symbol_names): circuit.append(cirq.H.on_each(*qubits)) hamiltonian_gate = cirq.BooleanHamiltonian( - [boolean_str], 0.1 * math.pi, ladder_target, symbol_names + qubits, [boolean_str], 0.1 * math.pi, ladder_target, symbol_names ) assert hamiltonian_gate.num_qubits() == n - circuit.append(cirq.decompose(hamiltonian_gate(*qubits))) + circuit.append(hamiltonian_gate) phi = cirq.Simulator().simulate(circuit, qubit_order=qubits, initial_state=0).state_vector() actual = np.arctan2(phi.real, phi.imag) - math.pi / 2.0 > 0.0 diff --git a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json index 5ace03045f7..a9ca9ffe7bf 100644 --- a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json +++ b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json @@ -1,6 +1,12 @@ [ { "cirq_type": "BooleanHamiltonian", + "qubits": [ + { + "cirq_type": "NamedQubit", + "name": "q0" + } + ], "boolean_strs": [ "q0" ], diff --git a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr index 7b90f08e5af..c7dd8d89d86 100644 --- a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr +++ b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr @@ -1 +1 @@ -[cirq.BooleanHamiltonian(['q0'], 0.20160913, False)] \ No newline at end of file +[cirq.BooleanHamiltonian([cirq.NamedQubit('q0')], ['q0'], 0.20160913, False)] \ No newline at end of file diff --git a/examples/qaoa.py b/examples/qaoa.py index 0b6a048875e..5c22ea88c40 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -29,8 +29,10 @@ def f(x): circuit.append(cirq.H.on_each(*qubits)) for i in range(p): - hamiltonian_gate = cirq.BooleanHamiltonian(booleans, 2.0 * x[p + i], ladder_target=True) - circuit.append(cirq.decompose(hamiltonian_gate(*qubits))) + hamiltonian_gate = cirq.BooleanHamiltonian( + qubits, booleans, 2.0 * x[p + i], ladder_target=True + ) + circuit.append(hamiltonian_gate) circuit.append(cirq.rx(2.0 * x[i]).on_each(*qubits)) circuit.append(cirq.measure(*qubits, key='m')) From 7a510b7c71e51a0a7886f3f6c90209b278065f1d Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 5 Jun 2021 05:46:43 +0000 Subject: [PATCH 56/87] Make get_name_to_id() private --- cirq-core/cirq/ops/boolean_hamiltonian.py | 92 ++++++++----------- .../cirq/ops/boolean_hamiltonian_test.py | 41 ++------- .../json_test_data/BooleanHamiltonian.json | 17 ++-- .../json_test_data/BooleanHamiltonian.repr | 2 +- examples/qaoa.py | 8 +- 5 files changed, 61 insertions(+), 99 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index ccab16dcf64..f5d73ef71ee 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -98,6 +98,23 @@ def Z(i: int) -> 'HamiltonianPolynomial': return HamiltonianPolynomial(defaultdict(float, {(i,): 1.0})) +def _get_name_to_id(boolean_exprs: Sequence[Expr]) -> Dict[str, int]: + """Maps the variables to a unique integer. + + Args: + boolean_expr: A Sympy expression containing symbols and Boolean operations + + Return: + A dictionary of string (the variable name) to a unique integer. + """ + + symbol_names = { + symbol.name for boolean_expr in boolean_exprs for symbol in boolean_expr.free_symbols + } + # For run-to-run identicalness, we sort the symbol name lexicographically. + return {symb: i for i, symb in enumerate(sorted(symbol_names))} + + def _build_hamiltonian_from_boolean( boolean_expr: Expr, name_to_id: Dict[str, int] ) -> HamiltonianPolynomial: @@ -106,7 +123,7 @@ def _build_hamiltonian_from_boolean( Args: boolean_expr: A Sympy expression containing symbols and Boolean operations name_to_id: A dictionary from symbol name to an integer, typically built by calling - get_name_to_id(). + _get_name_to_id(). Return: The HamiltonianPolynomial that represents the Boolean expression. @@ -315,11 +332,10 @@ class BooleanHamiltonian(raw_types.Operation): def __init__( self, - qubits: Sequence['cirq.Qid'], + qubit_map: Dict[str, 'cirq.Qid'], boolean_strs: Sequence[str], theta: float, ladder_target: bool, - symbol_names: Optional[Sequence[str]] = None, ): """ Builds an BooleanHamiltonian. @@ -339,88 +355,56 @@ def __init__( Args: boolean_strs: The list of Sympy-parsable Boolean expressions. - qubits: DO NOT SUBMIT + qubit_map: map of string (boolean variable name) to qubit. theta: The list of thetas to scale the Hamiltonian. ladder_target: Whether to use convention of figure 7a or 7b. - symbol_names: A list of symbol names that should be a superset of all the symbols used. """ - self._qubits: Sequence['cirq.Qid'] = qubits + self._qubit_map: Dict[str, 'cirq.Qid'] = qubit_map self._boolean_strs: Sequence[str] = boolean_strs self._theta: float = theta self._ladder_target: bool = ladder_target - self._symbol_names: Optional[Sequence[str]] = symbol_names - - boolean_exprs = [sympy_parser.parse_expr(boolean_str) for boolean_str in boolean_strs] - name_to_id = BooleanHamiltonian.get_name_to_id(boolean_exprs, symbol_names) - self._hamiltonian_polynomial_list = [ - _build_hamiltonian_from_boolean(boolean_expr, name_to_id) - for boolean_expr in boolean_exprs - ] - self._num_qubits = len(name_to_id) def with_qubits(self, *new_qubits: 'cirq.Qid') -> 'BooleanHamiltonian': return BooleanHamiltonian( - list(new_qubits), + {q.name(): q for q in new_qubits}, self._boolean_strs, self._theta, self._ladder_target, - self._symbol_names, ) @property def qubits(self) -> Tuple[raw_types.Qid, ...]: - return tuple(self._qubits) + return tuple(self._qubit_map.values()) def num_qubits(self) -> int: - return self._num_qubits - - @staticmethod - def get_name_to_id( - boolean_exprs: Sequence[Expr], symbol_names: Optional[Sequence[str]] = None - ) -> Dict[str, int]: - """Maps the variables to a unique integer. - - Args: - boolean_expr: A Sympy expression containing symbols and Boolean operations - symbol_names: A list of symbol names that should be a superset of all the symbols used. - - Return: - A dictionary of string (the variable name) to a unique integer. - """ - - required_symbol_names = { - symbol.name for boolean_expr in boolean_exprs for symbol in boolean_expr.free_symbols - } - # For run-to-run identicalness, we sort the symbol name lexicographically. - if symbol_names: - for symb in required_symbol_names: - if symb not in symbol_names: - raise ValueError( - 'symbol_names was specified but it is not a superset of the symbols ' - + f'in boolean_exprs. Missing required symbol: {symb}' - ) - return {symb: i for i, symb in enumerate(sorted(symbol_names))} - else: - return {symb: i for i, symb in enumerate(sorted(required_symbol_names))} + return len(self._qubit_map) def _value_equality_values_(self): - return self._qubits, self._boolean_strs, self._theta, self._ladder_target + return self._qubit_map, self._boolean_strs, self._theta, self._ladder_target def _json_dict_(self) -> Dict[str, Any]: return { 'cirq_type': self.__class__.__name__, - 'qubits': self._qubits, + 'qubit_map': self._qubit_map, 'boolean_strs': self._boolean_strs, 'theta': self._theta, 'ladder_target': self._ladder_target, - 'symbol_names': self._symbol_names, } @classmethod - def _from_json_dict_(cls, qubits, boolean_strs, theta, ladder_target, symbol_names, **kwargs): - return cls(qubits, boolean_strs, theta, ladder_target, symbol_names) + def _from_json_dict_(cls, qubit_map, boolean_strs, theta, ladder_target, **kwargs): + return cls(qubit_map, boolean_strs, theta, ladder_target) def _decompose_(self): + boolean_exprs = [sympy_parser.parse_expr(boolean_str) for boolean_str in self._boolean_strs] + name_to_id = _get_name_to_id(boolean_exprs) + hamiltonian_polynomial_list = [ + _build_hamiltonian_from_boolean(boolean_expr, name_to_id) + for boolean_expr in boolean_exprs + ] + + qubits = [self._qubit_map[name] for name in name_to_id.keys()] + yield _get_gates_from_hamiltonians( - self._hamiltonian_polynomial_list, self._qubits, self._theta, self._ladder_target + hamiltonian_polynomial_list, qubits, self._theta, self._ladder_target ) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index 2f5290adcf3..7ed223c2f95 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -24,44 +24,20 @@ ) def test_build_hamiltonian_from_boolean(boolean_expr, expected_hamiltonian_polynomial): boolean = sympy_parser.parse_expr(boolean_expr) - name_to_id = cirq.BooleanHamiltonian.get_name_to_id([boolean]) + name_to_id = bh._get_name_to_id([boolean]) actual = bh._build_hamiltonian_from_boolean(boolean, name_to_id) assert expected_hamiltonian_polynomial == str(actual) def test_unsupported_op(): not_a_boolean = sympy_parser.parse_expr('x * x') - name_to_id = cirq.BooleanHamiltonian.get_name_to_id([not_a_boolean]) + name_to_id = bh._get_name_to_id([not_a_boolean]) with pytest.raises(ValueError, match='Unsupported type'): bh._build_hamiltonian_from_boolean(not_a_boolean, name_to_id) @pytest.mark.parametrize( - 'boolean_strs,symbol_names,expected', - [ - (['x0'], None, {'x0': 0}), - (['x0 & x1'], None, {'x0': 0, 'x1': 1}), - (['x0', 'x1'], None, {'x0': 0, 'x1': 1}), - (['x1', 'x0'], None, {'x0': 0, 'x1': 1}), - (['x1', 'x0'], ['x2', 'x0', 'x1'], {'x0': 0, 'x1': 1, 'x2': 2}), - ], -) -def test_get_name_to_id(boolean_strs, symbol_names, expected): - assert ( - cirq.BooleanHamiltonian.get_name_to_id( - [sympy_parser.parse_expr(boolean_str) for boolean_str in boolean_strs], symbol_names - ) - == expected - ) - - -def test_get_name_to_id_missing_required_symbol(): - with pytest.raises(ValueError, match='Missing required symbol: x1'): - cirq.BooleanHamiltonian.get_name_to_id([sympy_parser.parse_expr('x1')], ['x2']) - - -@pytest.mark.parametrize( - 'boolean_str,ladder_target,symbol_names', + 'boolean_str,ladder_target', itertools.product( [ 'x0', @@ -83,15 +59,18 @@ def test_get_name_to_id_missing_required_symbol(): '(x0 ^ x1 ^ x2) | (x2 ^ x3 ^ x4)', '(x0 ^ x2 ^ x4) | (x1 ^ x2 ^ x3)', 'x0 & x1 & (x2 | x3)', + 'x0 & ~x2', + '~x0 & x2', + 'x2 & ~x0', + '~x2 & x0', '(x2 | x1) ^ x0', ], [False, True], - [None, ['x4', 'x0', 'x3', 'x2', 'x1']], ), ) -def test_circuit(boolean_str, ladder_target, symbol_names): +def test_circuit(boolean_str, ladder_target): boolean_expr = sympy_parser.parse_expr(boolean_str) - var_names = cirq.BooleanHamiltonian.get_name_to_id([boolean_expr], symbol_names) + var_names = cirq.parameter_names(boolean_expr) qubits = [cirq.NamedQubit(name) for name in var_names] @@ -110,7 +89,7 @@ def test_circuit(boolean_str, ladder_target, symbol_names): circuit.append(cirq.H.on_each(*qubits)) hamiltonian_gate = cirq.BooleanHamiltonian( - qubits, [boolean_str], 0.1 * math.pi, ladder_target, symbol_names + {q.name: q for q in qubits}, [boolean_str], 0.1 * math.pi, ladder_target ) assert hamiltonian_gate.num_qubits() == n diff --git a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json index a9ca9ffe7bf..c4f5017d725 100644 --- a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json +++ b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json @@ -1,17 +1,16 @@ [ { "cirq_type": "BooleanHamiltonian", - "qubits": [ - { - "cirq_type": "NamedQubit", - "name": "q0" - } - ], + "qubit_map": { + "q0": { + "cirq_type": "NamedQubit", + "name": "q0" + } + }, "boolean_strs": [ - "q0" + "q0" ], "theta": 0.20160913, - "ladder_target": false, - "symbol_names": null + "ladder_target": false } ] \ No newline at end of file diff --git a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr index c7dd8d89d86..a012efd2d2e 100644 --- a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr +++ b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr @@ -1 +1 @@ -[cirq.BooleanHamiltonian([cirq.NamedQubit('q0')], ['q0'], 0.20160913, False)] \ No newline at end of file +[cirq.BooleanHamiltonian({'q0': cirq.NamedQubit('q0')}, ['q0'], 0.20160913, False)] \ No newline at end of file diff --git a/examples/qaoa.py b/examples/qaoa.py index 5c22ea88c40..7c45b1aaa8f 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -20,8 +20,8 @@ def brute_force(graph, n): def qaoa(booleans, repetitions, maxiter, p): boolean_exprs = [parse_expr(boolean) for boolean in booleans] - name_to_id = cirq.BooleanHamiltonian.get_name_to_id(boolean_exprs) - qubits = [cirq.NamedQubit(name) for name in name_to_id.keys()] + param_names = cirq.parameter_names(boolean_exprs) + qubits = [cirq.NamedQubit(name) for name in param_names] def f(x): # Build the circuit. @@ -30,7 +30,7 @@ def f(x): for i in range(p): hamiltonian_gate = cirq.BooleanHamiltonian( - qubits, booleans, 2.0 * x[p + i], ladder_target=True + {q.name: q for q in qubits}, booleans, 2.0 * x[p + i], ladder_target=True ) circuit.append(hamiltonian_gate) circuit.append(cirq.rx(2.0 * x[i]).on_each(*qubits)) @@ -44,7 +44,7 @@ def f(x): # Evaluate values = [] for rep in range(repetitions): - subs = {name: val == 1 for name, val in zip(name_to_id.keys(), bitstrings[rep, :])} + subs = {name: val == 1 for name, val in zip(param_names, bitstrings[rep, :])} values.append( sum(1 if boolean_expr.subs(subs) else 0 for boolean_expr in boolean_exprs) ) From ccf429c35d51a15b57427990810964ef2cadc184 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 5 Jun 2021 07:24:03 +0000 Subject: [PATCH 57/87] Use PauliString --- cirq-core/cirq/ops/boolean_hamiltonian.py | 153 +++++------------- .../cirq/ops/boolean_hamiltonian_test.py | 21 ++- 2 files changed, 51 insertions(+), 123 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index f5d73ef71ee..8f70c71e780 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -13,8 +13,22 @@ from cirq.ops import raw_types -class HamiltonianPolynomial: - """A container class of Boolean function as equation (2) of [1] +def _Hamiltonian_O(): + return 0.0 * cirq.PauliString({}) + + +def _Hamiltonian_I(): + return cirq.PauliString({}) + + +def _Hamiltonian_Z(qubit): + return cirq.PauliString({qubit: cirq.Z}) + + +def _build_hamiltonian_from_boolean( + boolean_expr: Expr, qubit_map: Dict[str, 'cirq.Qid'] +) -> 'cirq.PauliString': + """Builds the Hamiltonian representation of Boolean expression as per [1]: It is essentially a polynomial of Pauli Zs on different qubits. For example, this object could represent the polynomial 0.5*I - 0.5*Z_1*Z_2 and it would be stored inside this object as: @@ -32,126 +46,37 @@ class HamiltonianPolynomial: [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, Alán Aspuru-Guzik, https://arxiv.org/abs/1306.3991 - """ - - def __init__(self, hamiltonians: Dict[Tuple[int, ...], float]): - # The representation is Tuple[int, ...] to weights. The tuple contains the integers of - # where Z_i is present. For example, Z_0.Z_3 would be (0, 3), and I is the empty tuple. - self._hamiltonians: DefaultDict[Tuple[int, ...], float] = defaultdict( - float, {h: w for h, w in hamiltonians.items() if math.fabs(w) > 1e-12} - ) - - @property - def hamiltonians(self) -> DefaultDict[Tuple[int, ...], float]: - return self._hamiltonians - - def __repr__(self): - # For run-to-run identicalness, we sort the keys lexicographically. - formatted_terms = [ - f"{self._hamiltonians[h]:.2f}*{'*'.join('Z_%d' % d for d in h) if h else 'I'}" - for h in sorted(self._hamiltonians) - ] - return "; ".join(formatted_terms) - - def __add__(self, other: 'HamiltonianPolynomial') -> 'HamiltonianPolynomial': - return self._signed_add(other, 1.0) - - def __sub__(self, other: 'HamiltonianPolynomial') -> 'HamiltonianPolynomial': - return self._signed_add(other, -1.0) - - def _signed_add(self, other: 'HamiltonianPolynomial', sign: float) -> 'HamiltonianPolynomial': - hamiltonians: DefaultDict[Tuple[int, ...], float] = self._hamiltonians.copy() - for h, w in other.hamiltonians.items(): - hamiltonians[h] += sign * w - return HamiltonianPolynomial(hamiltonians) - - def __rmul__(self, other: float) -> 'HamiltonianPolynomial': - return HamiltonianPolynomial( - defaultdict(float, {k: other * w for k, w in self._hamiltonians.items()}) - ) - - def __mul__(self, other: 'HamiltonianPolynomial') -> 'HamiltonianPolynomial': - hamiltonians: DefaultDict[Tuple[int, ...], float] = defaultdict(float, {}) - for h1, w1 in self._hamiltonians.items(): - for h2, w2 in other.hamiltonians.items(): - # Since we represent the Hamilonians using the indices of the Z_i, when we multiply - # two Hamiltionians, it's equivalent to doing an XOR of the two sets. For example, - # if h_A = Z_1 . Z_2 and h_B = Z_1 . Z_3 then the product is: - # h_A . h_B = Z_1 . Z_2 . Z_1 . Z_3 = Z_1 . Z_1 . Z_2 . Z_3 = Z_2 . Z_3 - # and thus, it is represented by (2, 3). In sort, we do the XOR / symmetric - # difference of the two tuples. - h = tuple(sorted(set(h1).symmetric_difference(h2))) - w = w1 * w2 - hamiltonians[h] += w - return HamiltonianPolynomial(hamiltonians) - - @staticmethod - def O() -> 'HamiltonianPolynomial': - return HamiltonianPolynomial(defaultdict(float, {})) - - @staticmethod - def I() -> 'HamiltonianPolynomial': - return HamiltonianPolynomial(defaultdict(float, {(): 1.0})) - - @staticmethod - def Z(i: int) -> 'HamiltonianPolynomial': - return HamiltonianPolynomial(defaultdict(float, {(i,): 1.0})) - - -def _get_name_to_id(boolean_exprs: Sequence[Expr]) -> Dict[str, int]: - """Maps the variables to a unique integer. Args: boolean_expr: A Sympy expression containing symbols and Boolean operations - - Return: - A dictionary of string (the variable name) to a unique integer. - """ - - symbol_names = { - symbol.name for boolean_expr in boolean_exprs for symbol in boolean_expr.free_symbols - } - # For run-to-run identicalness, we sort the symbol name lexicographically. - return {symb: i for i, symb in enumerate(sorted(symbol_names))} - - -def _build_hamiltonian_from_boolean( - boolean_expr: Expr, name_to_id: Dict[str, int] -) -> HamiltonianPolynomial: - """Builds the Hamiltonian representation of Boolean expression as per [1]: - - Args: - boolean_expr: A Sympy expression containing symbols and Boolean operations - name_to_id: A dictionary from symbol name to an integer, typically built by calling - _get_name_to_id(). + qubit_map: map of string (boolean variable name) to qubit. Return: The HamiltonianPolynomial that represents the Boolean expression. """ if isinstance(boolean_expr, Symbol): # Table 1 of [1], entry for 'x' is '1/2.I - 1/2.Z' - i = name_to_id[boolean_expr.name] - return 0.5 * HamiltonianPolynomial.I() - 0.5 * HamiltonianPolynomial.Z(i) + return 0.5 * _Hamiltonian_I() - 0.5 * _Hamiltonian_Z(qubit_map[boolean_expr.name]) if isinstance(boolean_expr, (And, Not, Or, Xor)): sub_hamiltonians = [ - _build_hamiltonian_from_boolean(sub_boolean_expr, name_to_id) + _build_hamiltonian_from_boolean(sub_boolean_expr, qubit_map) for sub_boolean_expr in boolean_expr.args ] # We apply the equalities of theorem 1 of [1]. if isinstance(boolean_expr, And): - hamiltonian = HamiltonianPolynomial.I() + hamiltonian = _Hamiltonian_I() for sub_hamiltonian in sub_hamiltonians: hamiltonian = hamiltonian * sub_hamiltonian elif isinstance(boolean_expr, Not): assert len(sub_hamiltonians) == 1 - hamiltonian = HamiltonianPolynomial.I() - sub_hamiltonians[0] + hamiltonian = _Hamiltonian_I() - sub_hamiltonians[0] elif isinstance(boolean_expr, Or): - hamiltonian = HamiltonianPolynomial.O() + hamiltonian = _Hamiltonian_O() for sub_hamiltonian in sub_hamiltonians: hamiltonian = hamiltonian + sub_hamiltonian - hamiltonian * sub_hamiltonian elif isinstance(boolean_expr, Xor): - hamiltonian = HamiltonianPolynomial.O() + hamiltonian = _Hamiltonian_O() for sub_hamiltonian in sub_hamiltonians: hamiltonian = hamiltonian + sub_hamiltonian - 2.0 * hamiltonian * sub_hamiltonian return hamiltonian @@ -182,8 +107,8 @@ def _gray_code_comparator(k1: Tuple[int, ...], k2: Tuple[int, ...], flip: bool = def _get_gates_from_hamiltonians( - hamiltonian_polynomial_list: List[HamiltonianPolynomial], - qubits: Sequence['cirq.Qid'], + hamiltonian_polynomial_list: List['cirq.PauliSum'], + qubit_map: Dict[str, 'cirq.Qid'], theta: float, ladder_target: bool = False, ): @@ -192,20 +117,28 @@ def _get_gates_from_hamiltonians( Args: hamiltonian_polynomial_list: the list of Hamiltonians, typically built by calling _build_hamiltonian_from_boolean(). - qubits: The list of qubits corresponding to the variables. + qubit_map: map of string (boolean variable name) to qubit. theta: A single float scaling the rotations. ladder_target: Whether to use convention of figure 7a or 7b. Yield: Gates that are the decomposition of the Hamiltonian. """ - combined = sum(hamiltonian_polynomial_list, HamiltonianPolynomial.O()) + combined: 'cirq.PauliSum' = sum(hamiltonian_polynomial_list, _Hamiltonian_O()) + + qubit_names = sorted(qubit_map.keys()) + qubits = [qubit_map[name] for name in qubit_names] + qubit_indices = {qubit: i for i, qubit in enumerate(qubits)} + + hamiltonians = {} + for pauli_string in combined: + w = pauli_string.coefficient.real + qubit_idx = tuple(sorted(qubit_indices[qubit] for qubit in pauli_string.qubits)) + hamiltonians[qubit_idx] = w # Here we follow improvements of [4] cancelling out the CNOTs. The first step is to order by # Gray code so that as few as possible gates are changed. - sorted_hs = sorted( - list(combined.hamiltonians.keys()), key=functools.cmp_to_key(_gray_code_comparator) - ) + sorted_hs = sorted(list(hamiltonians.keys()), key=functools.cmp_to_key(_gray_code_comparator)) def _simplify_cnots(cnots): _control = 0 @@ -313,7 +246,7 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): previous_h: Tuple[int, ...] = () for h in sorted_hs: - w = combined.hamiltonians[h] + w = hamiltonians[h] yield _apply_cnots(previous_h, h) @@ -397,14 +330,10 @@ def _from_json_dict_(cls, qubit_map, boolean_strs, theta, ladder_target, **kwarg def _decompose_(self): boolean_exprs = [sympy_parser.parse_expr(boolean_str) for boolean_str in self._boolean_strs] - name_to_id = _get_name_to_id(boolean_exprs) hamiltonian_polynomial_list = [ - _build_hamiltonian_from_boolean(boolean_expr, name_to_id) + _build_hamiltonian_from_boolean(boolean_expr, self._qubit_map) for boolean_expr in boolean_exprs ] - - qubits = [self._qubit_map[name] for name in name_to_id.keys()] - yield _get_gates_from_hamiltonians( - hamiltonian_polynomial_list, qubits, self._theta, self._ladder_target + hamiltonian_polynomial_list, self._qubit_map, self._theta, self._ladder_target ) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index 7ed223c2f95..a84f34b7c3d 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -14,26 +14,26 @@ @pytest.mark.parametrize( 'boolean_expr,expected_hamiltonian_polynomial', [ - ('x', '0.50*I; -0.50*Z_0'), - ('~x', '0.50*I; 0.50*Z_0'), - ('x0 ^ x1', '0.50*I; -0.50*Z_0*Z_1'), - ('x0 & x1', '0.25*I; -0.25*Z_0; 0.25*Z_0*Z_1; -0.25*Z_1'), - ('x0 | x1', '0.75*I; -0.25*Z_0; -0.25*Z_0*Z_1; -0.25*Z_1'), - ('x0 ^ x1 ^ x2', '0.50*I; -0.50*Z_0*Z_1*Z_2'), + ('x', '0.500*I-0.500*Z(x)'), + ('~x', '0.500*I+0.500*Z(x)'), + ('x0 ^ x1', '0.500*I-0.500*Z(x0)*Z(x1)'), + ('x0 & x1', '0.250*I-0.250*Z(x1)-0.250*Z(x0)+0.250*Z(x0)*Z(x1)'), + ('x0 | x1', '0.750*I-0.250*Z(x0)-0.250*Z(x1)-0.250*Z(x0)*Z(x1)'), + ('x0 ^ x1 ^ x2', '0.500*I-0.500*Z(x0)*Z(x1)*Z(x2)'), ], ) def test_build_hamiltonian_from_boolean(boolean_expr, expected_hamiltonian_polynomial): boolean = sympy_parser.parse_expr(boolean_expr) - name_to_id = bh._get_name_to_id([boolean]) - actual = bh._build_hamiltonian_from_boolean(boolean, name_to_id) + qubit_map = {name: cirq.NamedQubit(name) for name in sorted(cirq.parameter_names(boolean))} + actual = bh._build_hamiltonian_from_boolean(boolean, qubit_map) assert expected_hamiltonian_polynomial == str(actual) def test_unsupported_op(): not_a_boolean = sympy_parser.parse_expr('x * x') - name_to_id = bh._get_name_to_id([not_a_boolean]) + qubit_map = {name: cirq.NamedQubit(name) for name in cirq.parameter_names(not_a_boolean)} with pytest.raises(ValueError, match='Unsupported type'): - bh._build_hamiltonian_from_boolean(not_a_boolean, name_to_id) + bh._build_hamiltonian_from_boolean(not_a_boolean, qubit_map) @pytest.mark.parametrize( @@ -71,7 +71,6 @@ def test_unsupported_op(): def test_circuit(boolean_str, ladder_target): boolean_expr = sympy_parser.parse_expr(boolean_str) var_names = cirq.parameter_names(boolean_expr) - qubits = [cirq.NamedQubit(name) for name in var_names] # We use Sympy to evaluate the expression: From e93bd018540e738a971ee159c3a8a1b182164e31 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 5 Jun 2021 07:41:48 +0000 Subject: [PATCH 58/87] Some fixes --- cirq-core/cirq/ops/boolean_hamiltonian.py | 6 ++---- cirq-core/cirq/ops/boolean_hamiltonian_test.py | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 8f70c71e780..c7260049f19 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -1,7 +1,5 @@ -from collections import defaultdict import functools -import math -from typing import Any, DefaultDict, Dict, List, Optional, Sequence, Tuple +from typing import cast, Any, Dict, List, Sequence, Tuple from sympy.logic.boolalg import And, Not, Or, Xor from sympy.core.expr import Expr @@ -299,7 +297,7 @@ def __init__( def with_qubits(self, *new_qubits: 'cirq.Qid') -> 'BooleanHamiltonian': return BooleanHamiltonian( - {q.name(): q for q in new_qubits}, + {cast(cirq.NamedQubit, q).name: q for q in new_qubits}, self._boolean_strs, self._theta, self._ladder_target, diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index a84f34b7c3d..193bbb6fdbc 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -90,6 +90,7 @@ def test_circuit(boolean_str, ladder_target): hamiltonian_gate = cirq.BooleanHamiltonian( {q.name: q for q in qubits}, [boolean_str], 0.1 * math.pi, ladder_target ) + assert hamiltonian_gate.with_qubits(*qubits) == hamiltonian_gate assert hamiltonian_gate.num_qubits() == n From 51d24786b05aa493bb8768414991f495d6d1ee25 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Mon, 7 Jun 2021 23:41:13 +0000 Subject: [PATCH 59/87] de-brittle unit test --- .../cirq/ops/boolean_hamiltonian_test.py | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index 193bbb6fdbc..7d5fec03bac 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -14,19 +14,29 @@ @pytest.mark.parametrize( 'boolean_expr,expected_hamiltonian_polynomial', [ - ('x', '0.500*I-0.500*Z(x)'), - ('~x', '0.500*I+0.500*Z(x)'), - ('x0 ^ x1', '0.500*I-0.500*Z(x0)*Z(x1)'), - ('x0 & x1', '0.250*I-0.250*Z(x1)-0.250*Z(x0)+0.250*Z(x0)*Z(x1)'), - ('x0 | x1', '0.750*I-0.250*Z(x0)-0.250*Z(x1)-0.250*Z(x0)*Z(x1)'), - ('x0 ^ x1 ^ x2', '0.500*I-0.500*Z(x0)*Z(x1)*Z(x2)'), + ('x', ['(-0.5+0j)*Z(x)', '(0.5+0j)*I']), + ('~x', ['(0.5+0j)*I', '(0.5+0j)*Z(x)']), + ('x0 ^ x1', ['(-0.5+0j)*Z(x0)*Z(x1)', '(0.5+0j)*I']), + ( + 'x0 & x1', + ['(-0.25+0j)*Z(x0)', '(-0.25+0j)*Z(x1)', '(0.25+0j)*I', '(0.25+0j)*Z(x0)*Z(x1)'], + ), + ( + 'x0 | x1', + ['(-0.25+0j)*Z(x0)', '(-0.25+0j)*Z(x0)*Z(x1)', '(-0.25+0j)*Z(x1)', '(0.75+0j)*I'], + ), + ('x0 ^ x1 ^ x2', ['(-0.5+0j)*Z(x0)*Z(x1)*Z(x2)', '(0.5+0j)*I']), ], ) def test_build_hamiltonian_from_boolean(boolean_expr, expected_hamiltonian_polynomial): boolean = sympy_parser.parse_expr(boolean_expr) qubit_map = {name: cirq.NamedQubit(name) for name in sorted(cirq.parameter_names(boolean))} actual = bh._build_hamiltonian_from_boolean(boolean, qubit_map) - assert expected_hamiltonian_polynomial == str(actual) + # Instead of calling str() directly, first make sure that the items are sorted. This is to make + # the unit test more robut in case Sympy would result in a different parsing order. By sorting + # the individual items, we would have a canonical representation. + actual_items = list(sorted(str(pauli_string) for pauli_string in actual)) + assert expected_hamiltonian_polynomial == actual_items def test_unsupported_op(): From 3aadaa543362d4cefceea01896e222c0b06f6682 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 12 Jun 2021 02:20:30 +0000 Subject: [PATCH 60/87] Intermediate addressing of comments --- cirq-core/cirq/ops/boolean_hamiltonian.py | 202 +++++++++++----------- 1 file changed, 99 insertions(+), 103 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index c7260049f19..49217672829 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -1,3 +1,16 @@ +""" +Represents Boolean functions as a series of CNOT and rotation gates. The Boolean functions are +passed as Sympy expressions and then turned into an optimized set of gates. + +References: +[1] On the representation of Boolean and real functions as Hamiltonians for quantum computing + 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, Alán Aspuru-Guzik, https://arxiv.org/abs/1306.3991 +""" + import functools from typing import cast, Any, Dict, List, Sequence, Tuple @@ -28,22 +41,8 @@ def _build_hamiltonian_from_boolean( ) -> 'cirq.PauliString': """Builds the Hamiltonian representation of Boolean expression as per [1]: - It is essentially a polynomial of Pauli Zs on different qubits. For example, this object could - represent the polynomial 0.5*I - 0.5*Z_1*Z_2 and it would be stored inside this object as: - self._hamiltonians = {(): 0.5, (1, 2): 0.5}. - - While this object can represent any polynomial of Pauli Zs, in this file, it will be used to - represent a Boolean operation which has a unique representation as a polynomial. This object - only handle basic operation on the polynomials (e.g. multiplication). The construction of a - polynomial from a Boolean is performed by _build_hamiltonian_from_boolean(). - - References: - [1] On the representation of Boolean and real functions as Hamiltonians for quantum computing - 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, Alán Aspuru-Guzik, https://arxiv.org/abs/1306.3991 + We can represent any Boolean expression as a polynomial of Pauli Zs on different qubits. For + example, this object could represent the polynomial 0.5*I - 0.5*Z_1*Z_2. Args: boolean_expr: A Sympy expression containing symbols and Boolean operations @@ -104,6 +103,86 @@ def _gray_code_comparator(k1: Tuple[int, ...], k2: Tuple[int, ...], flip: bool = return _gray_code_comparator(k1[0:-1], k2[0:-1], not flip) +def _simplify_cnots(cnots: List[Tuple[int, int]]): + """ + Takes a series of CNOTs and tries to applies rule to cancel out gates. + + 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. + """ + _control = 0 + _target = 1 + + # As per equations 9 and 10 of [4], if all the targets (resp. controls) are the same, + # the cnots commute. Further, if the control (resp. targets) are the same, the cnots + # can be simplified away: + # CNOT(i, j) @ CNOT(k, j) = CNOT(k, j) @ CNOT(i, j) + # CNOT(i, k) @ CNOT(i, j) = CNOT(i, j) @ CNOT(i, k) + for x, y in [(_control, _target), (_target, _control)]: + i = 0 + qubit_to_index: Dict[int, int] = {} + for j in range(1, len(cnots)): + if cnots[i][x] != cnots[j][x]: + # The targets (resp. control) don't match, so we reset the search. + i = j + qubit_to_index = {cnots[j][y]: j} + continue + + if cnots[j][y] in qubit_to_index: + k = qubit_to_index[cnots[j][y]] + # 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][y]] = j + + # Here we apply the simplification of equation 11. Note that by flipping the control + # and target qubits, we have an equally valid identity: + # CNOT(i, j) @ CNOT(j, k) == CNOT(j, k) @ CNOT(i, k) @ CNOT(i, j) + # CNOT(j, i) @ CNOT(k, j) == CNOT(k, j) @ CNOT(k, i) @ CNOT(j, i) + for x, y in [(_control, _target), (_target, _control)]: + # We investigate potential pivots sequentially. + for j in range(1, len(cnots) - 1): + # First, we look back for as long as the targets (resp. triggers) 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][y] != cnots[j][y]: + break + # We take a note of the trigger (resp. target). + common_A[cnots[i][x]] = i + + # Next, we look forward for as long as the triggers (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][x] != cnots[k][x]: + break + # We take a note of the target (resp. trigger). + common_B[cnots[k][y]] = 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 _get_gates_from_hamiltonians( hamiltonian_polynomial_list: List['cirq.PauliSum'], qubit_map: Dict[str, 'cirq.Qid'], @@ -119,7 +198,7 @@ def _get_gates_from_hamiltonians( theta: A single float scaling the rotations. ladder_target: Whether to use convention of figure 7a or 7b. - Yield: + Yields: Gates that are the decomposition of the Hamiltonian. """ combined: 'cirq.PauliSum' = sum(hamiltonian_polynomial_list, _Hamiltonian_O()) @@ -138,91 +217,6 @@ def _get_gates_from_hamiltonians( # Gray code so that as few as possible gates are changed. sorted_hs = sorted(list(hamiltonians.keys()), key=functools.cmp_to_key(_gray_code_comparator)) - def _simplify_cnots(cnots): - _control = 0 - _target = 1 - - while True: - # As per equations 9 and 10 of [4], if all the targets (resp. controls) are the same, - # the cnots commute. Further, if the control (resp. targets) are the same, the cnots - # can be simplified away: - # CNOT(i, j) @ CNOT(k, j) = CNOT(k, j) @ CNOT(i, j) - # CNOT(i, k) @ CNOT(i, j) = CNOT(i, j) @ CNOT(i, k) - found_simplification = False - for x, y in [(_control, _target), (_target, _control)]: - i = 0 - qubit_to_index: Dict[int, int] = {} - for j in range(1, len(cnots)): - if cnots[i][x] != cnots[j][x]: - # The targets (resp. control) don't match, so we reset the search. - i = j - qubit_to_index = {cnots[j][y]: j} - continue - - if cnots[j][y] in qubit_to_index: - k = qubit_to_index[cnots[j][y]] - # 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] - found_simplification = True - break - - qubit_to_index[cnots[j][y]] = j - - if found_simplification: - continue - - # Here we apply the simplification of equation 11. Note that by flipping the control - # and target qubits, we have an equally valid identity: - # CNOT(i, j) @ CNOT(j, k) == CNOT(j, k) @ CNOT(i, k) @ CNOT(i, j) - # CNOT(j, i) @ CNOT(k, j) == CNOT(k, j) @ CNOT(k, i) @ CNOT(j, i) - for x, y in [(_control, _target), (_target, _control)]: - # We investigate potential pivots sequentially. - for j in range(1, len(cnots) - 1): - # First, we look back for as long as the targets (resp. triggers) 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][y] != cnots[j][y]: - break - # We take a note of the trigger (resp. target). - common_A[cnots[i][x]] = i - - # Next, we look forward for as long as the triggers (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][x] != cnots[k][x]: - break - # We take a note of the target (resp. trigger). - common_B[cnots[k][y]] = 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] - found_simplification = True - break - - if found_simplification: - break - if found_simplification: - break - - if found_simplification: - continue - break - - return cnots - def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): # This function applies in sequence the CNOTs from prevh and then currh. However, given # that the h are sorted in Gray ordering and that some cancel each other, we can reduce @@ -237,7 +231,9 @@ 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)) - cnots = _simplify_cnots(cnots) + found_simplification = True + while found_simplification: + found_simplification, cnots = _simplify_cnots(cnots) for gate in (cirq.CNOT(qubits[c], qubits[t]) for c, t in cnots): yield gate From 17086bc0eafb77d8f44729b227020d59b60f03fe Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 12 Jun 2021 04:41:25 +0000 Subject: [PATCH 61/87] Address more comments --- cirq-core/cirq/ops/boolean_hamiltonian.py | 213 +++++++++++++--------- 1 file changed, 126 insertions(+), 87 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 49217672829..dcee346fd4e 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -12,7 +12,8 @@ """ import functools -from typing import cast, Any, Dict, List, Sequence, Tuple +import itertools +from typing import cast, Any, Callable, Dict, List, Sequence, Tuple from sympy.logic.boolalg import And, Not, Or, Xor from sympy.core.expr import Expr @@ -23,17 +24,11 @@ from cirq import value from cirq.ops import raw_types - -def _Hamiltonian_O(): - return 0.0 * cirq.PauliString({}) - - -def _Hamiltonian_I(): - return cirq.PauliString({}) - - -def _Hamiltonian_Z(qubit): - return cirq.PauliString({qubit: cirq.Z}) +_hamiltonian_O: Callable[[], 'cirq.PauliString'] = lambda: 0.0 * cirq.PauliString({}) +_hamiltonian_I: Callable[[], 'cirq.PauliString'] = lambda: cirq.PauliString({}) +_hamiltonian_Z: Callable[['cirq.Qid'], 'cirq.PauliString'] = lambda qubit: cirq.PauliString( + {qubit: cirq.Z} +) def _build_hamiltonian_from_boolean( @@ -53,7 +48,7 @@ def _build_hamiltonian_from_boolean( """ if isinstance(boolean_expr, Symbol): # Table 1 of [1], entry for 'x' is '1/2.I - 1/2.Z' - return 0.5 * _Hamiltonian_I() - 0.5 * _Hamiltonian_Z(qubit_map[boolean_expr.name]) + return 0.5 * _hamiltonian_I() - 0.5 * _hamiltonian_Z(qubit_map[boolean_expr.name]) if isinstance(boolean_expr, (And, Not, Or, Xor)): sub_hamiltonians = [ @@ -62,18 +57,18 @@ def _build_hamiltonian_from_boolean( ] # We apply the equalities of theorem 1 of [1]. if isinstance(boolean_expr, And): - hamiltonian = _Hamiltonian_I() + hamiltonian = _hamiltonian_I() for sub_hamiltonian in sub_hamiltonians: hamiltonian = hamiltonian * sub_hamiltonian elif isinstance(boolean_expr, Not): assert len(sub_hamiltonians) == 1 - hamiltonian = _Hamiltonian_I() - sub_hamiltonians[0] + hamiltonian = _hamiltonian_I() - sub_hamiltonians[0] elif isinstance(boolean_expr, Or): - hamiltonian = _Hamiltonian_O() + hamiltonian = _hamiltonian_O() for sub_hamiltonian in sub_hamiltonians: hamiltonian = hamiltonian + sub_hamiltonian - hamiltonian * sub_hamiltonian elif isinstance(boolean_expr, Xor): - hamiltonian = _Hamiltonian_O() + hamiltonian = _hamiltonian_O() for sub_hamiltonian in sub_hamiltonians: hamiltonian = hamiltonian + sub_hamiltonian - 2.0 * hamiltonian * sub_hamiltonian return hamiltonian @@ -103,6 +98,108 @@ def _gray_code_comparator(k1: Tuple[int, ...], k2: Tuple[int, ...], flip: bool = return _gray_code_comparator(k1[0:-1], k2[0:-1], not flip) +def _simplify_cnots_pairs( + cnots: List[Tuple[int, int]], flip_control_and_target: bool +) -> List[Tuple[int, int]]: + """Simplifies CNOT pairs according to equations 9 and 10 of [4].Simplifies + + 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. + """ + x, y = (0, 1) if flip_control_and_target else (1, 0) + + i = 0 + qubit_to_index: Dict[int, int] = {} + for j in range(1, len(cnots)): + if cnots[i][x] != cnots[j][x]: + # The targets (resp. control) don't match, so we reset the search. + i = j + qubit_to_index = {cnots[j][y]: j} + continue + + if cnots[j][y] in qubit_to_index: + k = qubit_to_index[cnots[j][y]] + # 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][y]] = j + + return False, cnots + + +def _simplify_cnots_tripplets( + cnots: List[Tuple[int, int]], flip_control_and_target: bool +) -> List[Tuple[int, int]]: + """Simplifies CNOT pairs according to equations 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. + """ + x, y = (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][y] != cnots[j][y]: + break + # We take a note of the control (resp. target). + common_A[cnots[i][x]] = 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][x] != cnots[k][x]: + break + # We take a note of the target (resp. control). + common_B[cnots[k][y]] = 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]]): """ Takes a series of CNOTs and tries to applies rule to cancel out gates. @@ -114,73 +211,17 @@ def _simplify_cnots(cnots: List[Tuple[int, int]]): A Boolean saying whether a simplification has been found. The simplified list of CNOTs. """ - _control = 0 - _target = 1 - - # As per equations 9 and 10 of [4], if all the targets (resp. controls) are the same, - # the cnots commute. Further, if the control (resp. targets) are the same, the cnots - # can be simplified away: - # CNOT(i, j) @ CNOT(k, j) = CNOT(k, j) @ CNOT(i, j) - # CNOT(i, k) @ CNOT(i, j) = CNOT(i, j) @ CNOT(i, k) - for x, y in [(_control, _target), (_target, _control)]: - i = 0 - qubit_to_index: Dict[int, int] = {} - for j in range(1, len(cnots)): - if cnots[i][x] != cnots[j][x]: - # The targets (resp. control) don't match, so we reset the search. - i = j - qubit_to_index = {cnots[j][y]: j} - continue - - if cnots[j][y] in qubit_to_index: - k = qubit_to_index[cnots[j][y]] - # 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][y]] = j - - # Here we apply the simplification of equation 11. Note that by flipping the control - # and target qubits, we have an equally valid identity: - # CNOT(i, j) @ CNOT(j, k) == CNOT(j, k) @ CNOT(i, k) @ CNOT(i, j) - # CNOT(j, i) @ CNOT(k, j) == CNOT(k, j) @ CNOT(k, i) @ CNOT(j, i) - for x, y in [(_control, _target), (_target, _control)]: - # We investigate potential pivots sequentially. - for j in range(1, len(cnots) - 1): - # First, we look back for as long as the targets (resp. triggers) 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][y] != cnots[j][y]: - break - # We take a note of the trigger (resp. target). - common_A[cnots[i][x]] = i - - # Next, we look forward for as long as the triggers (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][x] != cnots[k][x]: - break - # We take a note of the target (resp. trigger). - common_B[cnots[k][y]] = 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 + found_simplification = True + while found_simplification: + for simplify_fn, flip_control_and_target in itertools.product( + [_simplify_cnots_pairs, _simplify_cnots_tripplets], [False, True] + ): + found_simplification, cnots = simplify_fn(cnots, flip_control_and_target) + if found_simplification: + break + + return cnots def _get_gates_from_hamiltonians( @@ -201,7 +242,7 @@ def _get_gates_from_hamiltonians( Yields: Gates that are the decomposition of the Hamiltonian. """ - combined: 'cirq.PauliSum' = sum(hamiltonian_polynomial_list, _Hamiltonian_O()) + combined: 'cirq.PauliSum' = sum(hamiltonian_polynomial_list, _hamiltonian_O()) qubit_names = sorted(qubit_map.keys()) qubits = [qubit_map[name] for name in qubit_names] @@ -231,9 +272,7 @@ 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)) - found_simplification = True - while found_simplification: - found_simplification, cnots = _simplify_cnots(cnots) + cnots = _simplify_cnots(cnots) for gate in (cirq.CNOT(qubits[c], qubits[t]) for c, t in cnots): yield gate From df9e733558f475ff6dd503a5875b58e4f9c5f217 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 12 Jun 2021 04:48:01 +0000 Subject: [PATCH 62/87] Attempt to fix types --- cirq-core/cirq/ops/boolean_hamiltonian.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index dcee346fd4e..f29e447dd32 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -100,7 +100,7 @@ def _gray_code_comparator(k1: Tuple[int, ...], k2: Tuple[int, ...], flip: bool = def _simplify_cnots_pairs( cnots: List[Tuple[int, int]], flip_control_and_target: bool -) -> List[Tuple[int, int]]: +) -> Tuple[bool, List[Tuple[int, int]]]: """Simplifies CNOT pairs according to equations 9 and 10 of [4].Simplifies CNOT(i, j) @ CNOT(k, j) = CNOT(k, j) @ CNOT(i, j) @@ -142,7 +142,7 @@ def _simplify_cnots_pairs( def _simplify_cnots_tripplets( cnots: List[Tuple[int, int]], flip_control_and_target: bool -) -> List[Tuple[int, int]]: +) -> Tuple[bool, List[Tuple[int, int]]]: """Simplifies CNOT pairs according to equations 11 of [4]. CNOT(i, j) @ CNOT(j, k) == CNOT(j, k) @ CNOT(i, k) @ CNOT(i, j) @@ -242,7 +242,7 @@ def _get_gates_from_hamiltonians( Yields: Gates that are the decomposition of the Hamiltonian. """ - combined: 'cirq.PauliSum' = sum(hamiltonian_polynomial_list, _hamiltonian_O()) + combined = sum(hamiltonian_polynomial_list, _hamiltonian_O()) qubit_names = sorted(qubit_map.keys()) qubits = [qubit_map[name] for name in qubit_names] From e3e783c48da4b93ac4c4a7ceb9440606e28238b0 Mon Sep 17 00:00:00 2001 From: "Antoine (Tony) Bruguier" Date: Mon, 14 Jun 2021 20:13:10 -0700 Subject: [PATCH 63/87] Update cirq-core/cirq/ops/boolean_hamiltonian.py Co-authored-by: Balint Pato --- cirq-core/cirq/ops/boolean_hamiltonian.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index f29e447dd32..cf50a08ec54 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -294,7 +294,7 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): @value.value_equality class BooleanHamiltonian(raw_types.Operation): - """A gate that applies a Hamiltonian from a set of Boolean functions.""" + """An operation that represents a Hamiltonian from a set of Boolean functions.""" def __init__( self, From 67a8bb8e3940ffa4e825b487c4d2cf003d9e7c2f Mon Sep 17 00:00:00 2001 From: "Antoine (Tony) Bruguier" Date: Mon, 14 Jun 2021 20:13:27 -0700 Subject: [PATCH 64/87] Update cirq-core/cirq/ops/boolean_hamiltonian.py Co-authored-by: Balint Pato --- cirq-core/cirq/ops/boolean_hamiltonian.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index cf50a08ec54..5acef69e544 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -160,7 +160,7 @@ def _simplify_cnots_tripplets( A Boolean that tells whether a simplification has been performed. The CNOT list, potentially simplified. """ - x, y = (0, 1) if flip_control_and_target else (1, 0) + 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): From ce57671e63a31d00c0f7a312f77cdab4d4c5931b Mon Sep 17 00:00:00 2001 From: "Antoine (Tony) Bruguier" Date: Mon, 14 Jun 2021 20:13:40 -0700 Subject: [PATCH 65/87] Update cirq-core/cirq/ops/boolean_hamiltonian.py Co-authored-by: Balint Pato --- cirq-core/cirq/ops/boolean_hamiltonian.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 5acef69e544..ada2bb5f0bc 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -140,7 +140,7 @@ def _simplify_cnots_pairs( return False, cnots -def _simplify_cnots_tripplets( +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 equations 11 of [4]. From 2e330327ff638959ec7f38f3365034645bb59feb Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Tue, 15 Jun 2021 03:14:38 +0000 Subject: [PATCH 66/87] Add some unit tests. More to do. --- cirq-core/cirq/ops/boolean_hamiltonian.py | 9 +++- .../cirq/ops/boolean_hamiltonian_test.py | 52 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index f29e447dd32..195a0df937d 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -101,7 +101,7 @@ def _gray_code_comparator(k1: Tuple[int, ...], k2: Tuple[int, ...], flip: bool = def _simplify_cnots_pairs( cnots: List[Tuple[int, int]], flip_control_and_target: bool ) -> Tuple[bool, List[Tuple[int, int]]]: - """Simplifies CNOT pairs according to equations 9 and 10 of [4].Simplifies + """Simplifies CNOT pairs according to equations 9 and 10 of [4]. CNOT(i, j) @ CNOT(k, j) = CNOT(k, j) @ CNOT(i, j) ───@─────── ───────@─── @@ -118,10 +118,15 @@ def _simplify_cnots_pairs( A Boolean that tells whether a simplification has been performed. The CNOT list, potentially simplified. """ + + # If input is empty, there is no simplification. + if not cnots: + return False, cnots + x, y = (0, 1) if flip_control_and_target else (1, 0) i = 0 - qubit_to_index: Dict[int, int] = {} + qubit_to_index: Dict[int, int] = {cnots[i][y]: i} for j in range(1, len(cnots)): if cnots[i][x] != cnots[j][x]: # The targets (resp. control) don't match, so we reset the search. diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index 7d5fec03bac..59a0edd2fe2 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -149,3 +149,55 @@ def test_gray_code_sorting(n_bits, expected_hs): ) 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_cnots_pairs( + input_cnots, input_flip_control_and_target, expected_simplified, expected_output_cnots +): + actual_simplified, actual_output_cnots = bh._simplify_cnots_pairs( + 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)]), + # DO NOT SUBMIT add more tests!!!!!!!!!!!!!! + ], +) +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_tripplets( + input_cnots, input_flip_control_and_target + ) + assert actual_simplified == expected_simplified + assert actual_output_cnots == expected_output_cnots From 778a4c2f0d1f94c9dad4fc381181ba0eff71d951 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Tue, 15 Jun 2021 04:03:45 +0000 Subject: [PATCH 67/87] Address more comments --- cirq-core/cirq/ops/boolean_hamiltonian.py | 86 ++++++++++++------- .../cirq/ops/boolean_hamiltonian_test.py | 23 ++++- examples/qaoa.py | 11 ++- 3 files changed, 85 insertions(+), 35 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 7fa64e43b4f..ee4f49a060f 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -1,3 +1,16 @@ +# Copyright 2021 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. """ Represents Boolean functions as a series of CNOT and rotation gates. The Boolean functions are passed as Sympy expressions and then turned into an optimized set of gates. @@ -13,7 +26,7 @@ import functools import itertools -from typing import cast, Any, Callable, Dict, List, Sequence, Tuple +from typing import cast, Any, Callable, Dict, Generator, List, Sequence, Tuple from sympy.logic.boolalg import And, Not, Or, Xor from sympy.core.expr import Expr @@ -98,7 +111,7 @@ def _gray_code_comparator(k1: Tuple[int, ...], k2: Tuple[int, ...], flip: bool = return _gray_code_comparator(k1[0:-1], k2[0:-1], not flip) -def _simplify_cnots_pairs( +def _simplify_commuting_cnots( cnots: List[Tuple[int, int]], flip_control_and_target: bool ) -> Tuple[bool, List[Tuple[int, int]]]: """Simplifies CNOT pairs according to equations 9 and 10 of [4]. @@ -120,27 +133,24 @@ def _simplify_cnots_pairs( """ # If input is empty, there is no simplification. - if not cnots: - return False, cnots - - x, y = (0, 1) if flip_control_and_target else (1, 0) + target, control = (0, 1) if flip_control_and_target else (1, 0) i = 0 - qubit_to_index: Dict[int, int] = {cnots[i][y]: i} + qubit_to_index: Dict[int, int] = {cnots[i][control]: i} if cnots else {} for j in range(1, len(cnots)): - if cnots[i][x] != cnots[j][x]: + 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][y]: j} + qubit_to_index = {cnots[j][control]: j} continue - if cnots[j][y] in qubit_to_index: - k = qubit_to_index[cnots[j][y]] + 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][y]] = j + qubit_to_index[cnots[j][control]] = j return False, cnots @@ -148,7 +158,7 @@ def _simplify_cnots_pairs( 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 equations 11 of [4]. + """Simplifies CNOT pairs according to equation 11 of [4]. CNOT(i, j) @ CNOT(j, k) == CNOT(j, k) @ CNOT(i, k) @ CNOT(i, j) ───@─────── ───────@───@─── @@ -171,31 +181,31 @@ def _simplify_cnots_triplets( 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] = {} + common_a: Dict[int, int] = {} for i in range(j - 1, -1, -1): - if cnots[i][y] != cnots[j][y]: + if cnots[i][control] != cnots[j][control]: break # We take a note of the control (resp. target). - common_A[cnots[i][x]] = i + 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] = {} + common_b: Dict[int, int] = {} for k in range(j + 1, len(cnots)): - if cnots[j][x] != cnots[k][x]: + if cnots[j][target] != cnots[k][target]: break # We take a note of the target (resp. control). - common_B[cnots[k][y]] = k + common_b[cnots[k][control]] = k # Among all the candidates, find if they have a match. - keys = common_A.keys() & common_B.keys() + keys = common_a.keys() & common_b.keys() for key in keys: - assert common_A[key] != common_B[key] + 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 != 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 @@ -205,9 +215,8 @@ def _simplify_cnots_triplets( return False, cnots -def _simplify_cnots(cnots: List[Tuple[int, int]]): - """ - Takes a series of CNOTs and tries to applies rule to cancel out gates. +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. Args: cnots: A list of CNOTs represented as tuples of integer (control, target). @@ -220,7 +229,7 @@ def _simplify_cnots(cnots: List[Tuple[int, int]]): found_simplification = True while found_simplification: for simplify_fn, flip_control_and_target in itertools.product( - [_simplify_cnots_pairs, _simplify_cnots_tripplets], [False, True] + [_simplify_commuting_cnots, _simplify_cnots_triplets], [False, True] ): found_simplification, cnots = simplify_fn(cnots, flip_control_and_target) if found_simplification: @@ -234,7 +243,7 @@ def _get_gates_from_hamiltonians( qubit_map: Dict[str, 'cirq.Qid'], theta: float, ladder_target: bool = False, -): +) -> Generator['cirq.Operation', None, None]: """Builds a circuit according to [1]. Args: @@ -242,7 +251,24 @@ def _get_gates_from_hamiltonians( _build_hamiltonian_from_boolean(). qubit_map: map of string (boolean variable name) to qubit. theta: A single float scaling the rotations. - ladder_target: Whether to use convention of figure 7a or 7b. + ladder_target: Whether to use convention of figure 7a or 7b of [4]. The two formulations + yield the same output, but can be simplified differently. + For example for 3 qubits, the ladder target would be: + ───@─────────── + │ + ───┼───@─────── + │ │ + ───┼───┼───@─── + │ │ │ + ───X───X───X─── + Otherwise, it would be: + ───@─────────── + │ + ───X───@─────── + │ + ───────X───@─── + │ + ───────────X─── Yields: Gates that are the decomposition of the Hamiltonian. diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index 59a0edd2fe2..3855f953e48 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -1,3 +1,16 @@ +# Copyright 2021 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 @@ -171,10 +184,10 @@ def test_gray_code_comparison(seq_a, seq_b, expected): ([(0, 1), (2, 1), (0, 1)], True, False, [(0, 1), (2, 1), (0, 1)]), ], ) -def test_simplify_cnots_pairs( +def test_simplify_commuting_cnots( input_cnots, input_flip_control_and_target, expected_simplified, expected_output_cnots ): - actual_simplified, actual_output_cnots = bh._simplify_cnots_pairs( + actual_simplified, actual_output_cnots = bh._simplify_commuting_cnots( input_cnots, input_flip_control_and_target ) assert actual_simplified == expected_simplified @@ -190,13 +203,15 @@ def test_simplify_cnots_pairs( # Single CNOTs don't get simplified. ([(0, 1)], False, False, [(0, 1)]), ([(0, 1)], True, False, [(0, 1)]), - # DO NOT SUBMIT add more tests!!!!!!!!!!!!!! + # 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_tripplets( + actual_simplified, actual_output_cnots = bh._simplify_cnots_triplets( input_cnots, input_flip_control_and_target ) assert actual_simplified == expected_simplified diff --git a/examples/qaoa.py b/examples/qaoa.py index 7c45b1aaa8f..5167885f90b 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -1,5 +1,6 @@ """Runs the Quantum Approximate Optimization Algorithm on Max-Cut.""" import itertools +from typing import List import numpy as np import networkx @@ -18,7 +19,15 @@ def brute_force(graph, n): return max(np.round(vals)) -def qaoa(booleans, repetitions, maxiter, p): +def qaoa(booleans: List[str], repetitions: int, maxiter: int, p: int): + """Run the QAOA optimization for a list of Boolean expressions. + + Args: + booleans: A list of Boolean expressions (we want as many of them to be true). + repetitions: How many times to repeat the measurements. + maxiter: The number of iterations of the optimizer. + p: The number of times to repeat the Hamiltonian gate. + """ boolean_exprs = [parse_expr(boolean) for boolean in booleans] param_names = cirq.parameter_names(boolean_exprs) qubits = [cirq.NamedQubit(name) for name in param_names] From 2a6ffde22d82b67f7f03301daa57c40b5be13ab6 Mon Sep 17 00:00:00 2001 From: "Antoine (Tony) Bruguier" Date: Tue, 22 Jun 2021 10:52:09 -0700 Subject: [PATCH 68/87] Update cirq-core/cirq/ops/boolean_hamiltonian.py Co-authored-by: Balint Pato --- cirq-core/cirq/ops/boolean_hamiltonian.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index ee4f49a060f..119c347684c 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -57,7 +57,7 @@ def _build_hamiltonian_from_boolean( qubit_map: map of string (boolean variable name) to qubit. Return: - The HamiltonianPolynomial that represents the Boolean expression. + The PauliString that represents the Boolean expression. """ if isinstance(boolean_expr, Symbol): # Table 1 of [1], entry for 'x' is '1/2.I - 1/2.Z' From 08cac1aeeae7a28aba2ccb949461bd009cb6d4ad Mon Sep 17 00:00:00 2001 From: "Antoine (Tony) Bruguier" Date: Tue, 22 Jun 2021 10:52:21 -0700 Subject: [PATCH 69/87] Update examples/qaoa.py Co-authored-by: Balint Pato --- examples/qaoa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/qaoa.py b/examples/qaoa.py index 5167885f90b..c88fd0cf9b4 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -24,7 +24,7 @@ def qaoa(booleans: List[str], repetitions: int, maxiter: int, p: int): Args: booleans: A list of Boolean expressions (we want as many of them to be true). - repetitions: How many times to repeat the measurements. + repetitions: The number of times to repeat the measurements. maxiter: The number of iterations of the optimizer. p: The number of times to repeat the Hamiltonian gate. """ From ce989396627b8b9b12269d4652ea3c65b905862f Mon Sep 17 00:00:00 2001 From: "Antoine (Tony) Bruguier" Date: Tue, 22 Jun 2021 10:52:31 -0700 Subject: [PATCH 70/87] Update examples/qaoa.py Co-authored-by: Balint Pato --- examples/qaoa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/qaoa.py b/examples/qaoa.py index c88fd0cf9b4..9b5e24e7766 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -23,7 +23,7 @@ def qaoa(booleans: List[str], repetitions: int, maxiter: int, p: int): """Run the QAOA optimization for a list of Boolean expressions. Args: - booleans: A list of Boolean expressions (we want as many of them to be true). + booleans: A list of Boolean expressions (we want as many of them to be true as possible). repetitions: The number of times to repeat the measurements. maxiter: The number of iterations of the optimizer. p: The number of times to repeat the Hamiltonian gate. From a02b6f2ae306e7dfe1bb38e426ac040fbd3b8a72 Mon Sep 17 00:00:00 2001 From: "Antoine (Tony) Bruguier" Date: Tue, 22 Jun 2021 11:20:41 -0700 Subject: [PATCH 71/87] Update cirq-core/cirq/ops/boolean_hamiltonian.py Co-authored-by: Balint Pato --- cirq-core/cirq/ops/boolean_hamiltonian.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 119c347684c..d816806f5ff 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -253,7 +253,7 @@ def _get_gates_from_hamiltonians( theta: A single float scaling the rotations. ladder_target: Whether to use convention of figure 7a or 7b of [4]. The two formulations yield the same output, but can be simplified differently. - For example for 3 qubits, the ladder target would be: + For example for 3 qubits, the `ladder_target=True` results in: ───@─────────── │ ───┼───@─────── From 0afb1f452fb892d17362365076b6da9cc334a5a7 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Tue, 22 Jun 2021 18:35:25 +0000 Subject: [PATCH 72/87] Address some comments --- cirq-core/cirq/ops/boolean_hamiltonian.py | 43 ++++++++++++----------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 119c347684c..9c32a8a6f2f 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -251,24 +251,7 @@ def _get_gates_from_hamiltonians( _build_hamiltonian_from_boolean(). qubit_map: map of string (boolean variable name) to qubit. theta: A single float scaling the rotations. - ladder_target: Whether to use convention of figure 7a or 7b of [4]. The two formulations - yield the same output, but can be simplified differently. - For example for 3 qubits, the ladder target would be: - ───@─────────── - │ - ───┼───@─────── - │ │ - ───┼───┼───@─── - │ │ │ - ───X───X───X─── - Otherwise, it would be: - ───@─────────── - │ - ───X───@─────── - │ - ───────X───@─── - │ - ───────────X─── + ladder_target: has the same meaning as on BooleanHamiltonian. Yields: Gates that are the decomposition of the Hamiltonian. @@ -334,8 +317,7 @@ def __init__( theta: float, ladder_target: bool, ): - """ - Builds an BooleanHamiltonian. + """Builds an BooleanHamiltonian. For each element of a sequence of Boolean expressions, the code first transforms it into a polynomial of Pauli Zs that represent that particular expression. Then, we sum all the @@ -354,7 +336,26 @@ def __init__( boolean_strs: The list of Sympy-parsable Boolean expressions. qubit_map: map of string (boolean variable name) to qubit. theta: The list of thetas to scale the Hamiltonian. - ladder_target: Whether to use convention of figure 7a or 7b. + ladder_target: Whether to use convention of figure 7a or 7b of [4]. The two + formulations yield the same output, but can be simplified differently. + For example for 3 qubits, the ladder target would be: + ───@─────────── + │ + ───X───@─────── + │ + ───────X───@─── + │ + ───────────X─── + Otherwise, it would be: + ───@─────────── + │ + ───┼───@─────── + │ │ + ───┼───┼───@─── + │ │ │ + ───X───X───X─── + The papers do not provide a formula for which option yields a smaller number of + gates, so the option is provided as an input. """ self._qubit_map: Dict[str, 'cirq.Qid'] = qubit_map self._boolean_strs: Sequence[str] = boolean_strs From 2362d9d1120fdae1a1c5751cbd5aa5224dbaaae3 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Tue, 22 Jun 2021 18:56:26 +0000 Subject: [PATCH 73/87] Add back the examples --- examples/qaoa.py | 79 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/examples/qaoa.py b/examples/qaoa.py index 9b5e24e7766..41e99b9af1d 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -1,4 +1,81 @@ -"""Runs the Quantum Approximate Optimization Algorithm on Max-Cut.""" +"""Runs the Quantum Approximate Optimization Algorithm on Max-Cut. + +=== EXAMPLE CIRCUIT === + x0 x1 x2 x3 + │ │ │ │ + H H H H + │ │ │ │ + │ │ Y^-0.5 Y^-0.5 + │ │ │ │ + │ @─────@ │ + │ │ │ │ + │ │ Y^0.5 │ + │ │ │ │ + │ │ Rz(0) │ + │ │ │ │ + │ │ Y^-0.5 │ + │ │ │ │ + │ @─────@ │ + │ │ │ │ +┌╴│ │ │ │ ╶┐ +│ │ │ Y^0.5 │ │ +│ │ @─────┼──────@ │ +└╴│ │ │ │ ╶┘ + │ │ │ │ + │ │ Y^-0.5 Y^0.5 + │ │ │ │ + @─────┼─────@ Rz(0) + │ │ │ │ + │ │ Y^0.5 Y^-0.5 + │ │ │ │ +┌╴│ │ │ │ ╶┐ +│ │ │ Rz(0) │ │ +│ │ @─────┼──────@ │ +└╴│ │ │ │ ╶┘ + │ │ │ │ + │ Rx(0) Y^-0.5 Y^0.5 + │ │ │ │ + @─────┼─────@ Y^-0.5 + │ │ │ │ +┌╴│ │ │ │ ╶┐ +│ │ │ Y^0.5 │ │ +│ @─────┼─────┼──────@ │ +└╴│ │ │ │ ╶┘ + │ │ │ │ + │ │ Rx(0) Y^0.5 + │ │ │ │ + │ │ │ Rz(0) + │ │ │ │ + │ │ │ Y^-0.5 + │ │ │ │ + @─────┼─────┼──────@ + │ │ │ │ + Rx(0) │ │ Y^0.5 + │ │ │ │ + │ │ │ Rx(0) + │ │ │ │ + M─────M─────M──────M('m') + │ │ │ │ + +=== EXAMPLE OUTPUT === +Brute force max cut: 16 +μ=7.80 max=12 +μ=7.40 max=8 +μ=8.60 max=10 +μ=8.00 max=10 +μ=8.80 max=12 +μ=8.20 max=12 +μ=10.00 max=16 +μ=8.00 max=16 +μ=4.40 max=12 +μ=5.80 max=12 + + Return from subroutine COBYLA because the MAXFUN limit has been reached. + + NFVALS = 10 F =-1.000000E+01 MAXCV = 0.000000E+00 + X = 0.000000E+00 1.000000E+00 0.000000E+00 1.000000E+00 0.000000E+00 + 1.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +""" import itertools from typing import List From a6b1c8ec2bf3e8a5bb523b21547376309733e905 Mon Sep 17 00:00:00 2001 From: "Antoine (Tony) Bruguier" Date: Thu, 1 Jul 2021 20:24:36 -0700 Subject: [PATCH 74/87] Update cirq-core/cirq/ops/boolean_hamiltonian.py Co-authored-by: Balint Pato --- cirq-core/cirq/ops/boolean_hamiltonian.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 44c05f8fa2b..3c15618c6b7 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -334,7 +334,7 @@ def __init__( theta: float, ladder_target: bool, ): - """Builds an BooleanHamiltonian. + """Builds a BooleanHamiltonian. For each element of a sequence of Boolean expressions, the code first transforms it into a polynomial of Pauli Zs that represent that particular expression. Then, we sum all the From 3fe8a12afb3c76fd3048a7520684f071347fc203 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 2 Jul 2021 03:25:56 +0000 Subject: [PATCH 75/87] Find min gate num --- cirq-core/cirq/ops/boolean_hamiltonian.py | 33 ++++++++++++++++--- .../cirq/ops/boolean_hamiltonian_test.py | 2 +- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 44c05f8fa2b..06cf368f239 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -26,7 +26,8 @@ import functools import itertools -from typing import cast, Any, Callable, Dict, Generator, List, Sequence, Tuple +from typing import cast, Any, Callable, Dict, Generator, List, Optional, Sequence, Tuple +import sys from sympy.logic.boolalg import And, Not, Or, Xor from sympy.core.expr import Expr @@ -323,6 +324,18 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): yield _apply_cnots(previous_h, ()) +def shortest_generator( + generators: List[Generator['cirq.Operation', None, None]] +) -> Generator['cirq.Operation', None, None]: + min_gen_length = sys.maxsize + for generator in generators: + out_gen, gen_copy = itertools.tee(generator) + gen_length = sum(1 for _ in gen_copy) + if gen_length < min_gen_length: + min_gen = out_gen + return min_gen + + @value.value_equality class BooleanHamiltonian(raw_types.Operation): """An operation that represents a Hamiltonian from a set of Boolean functions.""" @@ -332,7 +345,7 @@ def __init__( qubit_map: Dict[str, 'cirq.Qid'], boolean_strs: Sequence[str], theta: float, - ladder_target: bool, + ladder_target: Optional[bool] = None, ): """Builds an BooleanHamiltonian. @@ -373,11 +386,13 @@ def __init__( ───X───X───X─── The papers do not provide a formula for which option yields a smaller number of gates, so the option is provided as an input. + If set to None, computes the set of gates for both approaches, count the number of + gates, and return the smallest set. """ self._qubit_map: Dict[str, 'cirq.Qid'] = qubit_map self._boolean_strs: Sequence[str] = boolean_strs self._theta: float = theta - self._ladder_target: bool = ladder_target + self._ladder_target: Optional[bool] = ladder_target def with_qubits(self, *new_qubits: 'cirq.Qid') -> 'BooleanHamiltonian': return BooleanHamiltonian( @@ -416,6 +431,14 @@ def _decompose_(self): _build_hamiltonian_from_boolean(boolean_expr, self._qubit_map) for boolean_expr in boolean_exprs ] - yield _get_gates_from_hamiltonians( - hamiltonian_polynomial_list, self._qubit_map, self._theta, self._ladder_target + + ladder_target_list = [True, False] if self._ladder_target is None else [self._ladder_target] + + return shortest_generator( + [ + _get_gates_from_hamiltonians( + hamiltonian_polynomial_list, self._qubit_map, self._theta, self._ladder_target + ) + for ladder_target in ladder_target_list + ] ) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index 3855f953e48..c83cfc14cc5 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -88,7 +88,7 @@ def test_unsupported_op(): '~x2 & x0', '(x2 | x1) ^ x0', ], - [False, True], + [False, True, None], ), ) def test_circuit(boolean_str, ladder_target): From cfea2a7fb78f21a219383e1062d019e68b5627c5 Mon Sep 17 00:00:00 2001 From: "Antoine (Tony) Bruguier" Date: Thu, 1 Jul 2021 20:28:52 -0700 Subject: [PATCH 76/87] Update cirq-core/cirq/ops/boolean_hamiltonian.py Co-authored-by: Balint Pato --- cirq-core/cirq/ops/boolean_hamiltonian.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 77c3e96309e..5880d84a987 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -115,7 +115,19 @@ def _gray_code_comparator(k1: Tuple[int, ...], k2: Tuple[int, ...], flip: bool = def _simplify_commuting_cnots( cnots: List[Tuple[int, int]], flip_control_and_target: bool ) -> Tuple[bool, List[Tuple[int, int]]]: - """Simplifies CNOT pairs according to equations 9 and 10 of [4]. + """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) ───@─────── ───────@─── From 43544699bb369369e5466f128ba3a9c3f4f1274b Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 2 Jul 2021 03:36:30 +0000 Subject: [PATCH 77/87] nit --- cirq-core/cirq/ops/boolean_hamiltonian.py | 228 +++++++++++----------- 1 file changed, 114 insertions(+), 114 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 5880d84a987..b2caf50b701 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -45,6 +45,114 @@ ) +@value.value_equality +class BooleanHamiltonian(raw_types.Operation): + """An operation that represents a Hamiltonian from a set of Boolean functions.""" + + def __init__( + self, + qubit_map: Dict[str, 'cirq.Qid'], + boolean_strs: Sequence[str], + theta: float, + ladder_target: Optional[bool] = None, + ): + """Builds a BooleanHamiltonian. + + For each element of a sequence of Boolean expressions, the code first transforms it into a + polynomial of Pauli Zs that represent that particular expression. Then, we sum all the + polynomials, thus making a function that goes from a series to Boolean inputs to an integer + that is the number of Boolean expressions that are true. + + For example, if we were using this gate for the max-cut problem that is typically used to + demonstrate the QAOA algorithm, there would be one Boolean expression per edge. Each + Boolean expression would be true iff the vertices on that are in different cuts (i.e. it's) + an XOR. + + Then, we compute exp(j * theta * polynomial), which is unitary because the polynomial is + Hermitian. + + Args: + boolean_strs: The list of Sympy-parsable Boolean expressions. + qubit_map: map of string (boolean variable name) to qubit. + theta: The list of thetas to scale the Hamiltonian. + ladder_target: Whether to use convention of figure 7a or 7b of [4]. The two + formulations yield the same output, but can be simplified differently. + For example for 3 qubits, the ladder target would be: + ───@─────────── + │ + ───X───@─────── + │ + ───────X───@─── + │ + ───────────X─── + Otherwise, it would be: + ───@─────────── + │ + ───┼───@─────── + │ │ + ───┼───┼───@─── + │ │ │ + ───X───X───X─── + The papers do not provide a formula for which option yields a smaller number of + gates, so the option is provided as an input. + If set to None, computes the set of gates for both approaches, count the number of + gates, and return the smallest set. + """ + self._qubit_map: Dict[str, 'cirq.Qid'] = qubit_map + self._boolean_strs: Sequence[str] = boolean_strs + self._theta: float = theta + self._ladder_target: Optional[bool] = ladder_target + + def with_qubits(self, *new_qubits: 'cirq.Qid') -> 'BooleanHamiltonian': + return BooleanHamiltonian( + {cast(cirq.NamedQubit, q).name: q for q in new_qubits}, + self._boolean_strs, + self._theta, + self._ladder_target, + ) + + @property + def qubits(self) -> Tuple[raw_types.Qid, ...]: + return tuple(self._qubit_map.values()) + + def num_qubits(self) -> int: + return len(self._qubit_map) + + def _value_equality_values_(self): + return self._qubit_map, self._boolean_strs, self._theta, self._ladder_target + + def _json_dict_(self) -> Dict[str, Any]: + return { + 'cirq_type': self.__class__.__name__, + 'qubit_map': self._qubit_map, + 'boolean_strs': self._boolean_strs, + 'theta': self._theta, + 'ladder_target': self._ladder_target, + } + + @classmethod + def _from_json_dict_(cls, qubit_map, boolean_strs, theta, ladder_target, **kwargs): + return cls(qubit_map, boolean_strs, theta, ladder_target) + + def _decompose_(self): + boolean_exprs = [sympy_parser.parse_expr(boolean_str) for boolean_str in self._boolean_strs] + hamiltonian_polynomial_list = [ + _build_hamiltonian_from_boolean(boolean_expr, self._qubit_map) + for boolean_expr in boolean_exprs + ] + + ladder_target_list = [True, False] if self._ladder_target is None else [self._ladder_target] + + return _shortest_generator( + [ + _get_gates_from_hamiltonians( + hamiltonian_polynomial_list, self._qubit_map, self._theta, self._ladder_target + ) + for ladder_target in ladder_target_list + ] + ) + + def _build_hamiltonian_from_boolean( boolean_expr: Expr, qubit_map: Dict[str, 'cirq.Qid'] ) -> 'cirq.PauliString': @@ -116,9 +224,10 @@ 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: + + 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─── @@ -126,7 +235,7 @@ def _simplify_commuting_cnots( ───@───@─── = ───@───@─── │ │ ───────X─── ───X─────── - + When flip_control_target=False: CNOT(i, j) @ CNOT(k, j) = CNOT(k, j) @ CNOT(i, j) @@ -145,7 +254,6 @@ def _simplify_commuting_cnots( The CNOT list, potentially simplified. """ - # If input is empty, there is no simplification. target, control = (0, 1) if flip_control_and_target else (1, 0) i = 0 @@ -336,7 +444,7 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): yield _apply_cnots(previous_h, ()) -def shortest_generator( +def _shortest_generator( generators: List[Generator['cirq.Operation', None, None]] ) -> Generator['cirq.Operation', None, None]: min_gen_length = sys.maxsize @@ -346,111 +454,3 @@ def shortest_generator( if gen_length < min_gen_length: min_gen = out_gen return min_gen - - -@value.value_equality -class BooleanHamiltonian(raw_types.Operation): - """An operation that represents a Hamiltonian from a set of Boolean functions.""" - - def __init__( - self, - qubit_map: Dict[str, 'cirq.Qid'], - boolean_strs: Sequence[str], - theta: float, - ladder_target: Optional[bool] = None, - ): - """Builds a BooleanHamiltonian. - - For each element of a sequence of Boolean expressions, the code first transforms it into a - polynomial of Pauli Zs that represent that particular expression. Then, we sum all the - polynomials, thus making a function that goes from a series to Boolean inputs to an integer - that is the number of Boolean expressions that are true. - - For example, if we were using this gate for the max-cut problem that is typically used to - demonstrate the QAOA algorithm, there would be one Boolean expression per edge. Each - Boolean expression would be true iff the vertices on that are in different cuts (i.e. it's) - an XOR. - - Then, we compute exp(j * theta * polynomial), which is unitary because the polynomial is - Hermitian. - - Args: - boolean_strs: The list of Sympy-parsable Boolean expressions. - qubit_map: map of string (boolean variable name) to qubit. - theta: The list of thetas to scale the Hamiltonian. - ladder_target: Whether to use convention of figure 7a or 7b of [4]. The two - formulations yield the same output, but can be simplified differently. - For example for 3 qubits, the ladder target would be: - ───@─────────── - │ - ───X───@─────── - │ - ───────X───@─── - │ - ───────────X─── - Otherwise, it would be: - ───@─────────── - │ - ───┼───@─────── - │ │ - ───┼───┼───@─── - │ │ │ - ───X───X───X─── - The papers do not provide a formula for which option yields a smaller number of - gates, so the option is provided as an input. - If set to None, computes the set of gates for both approaches, count the number of - gates, and return the smallest set. - """ - self._qubit_map: Dict[str, 'cirq.Qid'] = qubit_map - self._boolean_strs: Sequence[str] = boolean_strs - self._theta: float = theta - self._ladder_target: Optional[bool] = ladder_target - - def with_qubits(self, *new_qubits: 'cirq.Qid') -> 'BooleanHamiltonian': - return BooleanHamiltonian( - {cast(cirq.NamedQubit, q).name: q for q in new_qubits}, - self._boolean_strs, - self._theta, - self._ladder_target, - ) - - @property - def qubits(self) -> Tuple[raw_types.Qid, ...]: - return tuple(self._qubit_map.values()) - - def num_qubits(self) -> int: - return len(self._qubit_map) - - def _value_equality_values_(self): - return self._qubit_map, self._boolean_strs, self._theta, self._ladder_target - - def _json_dict_(self) -> Dict[str, Any]: - return { - 'cirq_type': self.__class__.__name__, - 'qubit_map': self._qubit_map, - 'boolean_strs': self._boolean_strs, - 'theta': self._theta, - 'ladder_target': self._ladder_target, - } - - @classmethod - def _from_json_dict_(cls, qubit_map, boolean_strs, theta, ladder_target, **kwargs): - return cls(qubit_map, boolean_strs, theta, ladder_target) - - def _decompose_(self): - boolean_exprs = [sympy_parser.parse_expr(boolean_str) for boolean_str in self._boolean_strs] - hamiltonian_polynomial_list = [ - _build_hamiltonian_from_boolean(boolean_expr, self._qubit_map) - for boolean_expr in boolean_exprs - ] - - ladder_target_list = [True, False] if self._ladder_target is None else [self._ladder_target] - - return shortest_generator( - [ - _get_gates_from_hamiltonians( - hamiltonian_polynomial_list, self._qubit_map, self._theta, self._ladder_target - ) - for ladder_target in ladder_target_list - ] - ) From de8463fb8b10e07897a255841f6426de2b51837b Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 2 Jul 2021 04:07:18 +0000 Subject: [PATCH 78/87] attempt fix mypy --- cirq-core/cirq/ops/boolean_hamiltonian.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index b2caf50b701..00e979f1ca9 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -26,7 +26,7 @@ import functools import itertools -from typing import cast, Any, Callable, Dict, Generator, List, Optional, Sequence, Tuple +from typing import cast, Any, Callable, Dict, Generator, Iterator, List, Optional, Sequence, Tuple import sys from sympy.logic.boolalg import And, Not, Or, Xor @@ -446,7 +446,7 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): def _shortest_generator( generators: List[Generator['cirq.Operation', None, None]] -) -> Generator['cirq.Operation', None, None]: +) -> Iterator['cirq.Operation']: min_gen_length = sys.maxsize for generator in generators: out_gen, gen_copy = itertools.tee(generator) From 4ba558e0ed39c94b50fef911437bc4ea245b6799 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 3 Jul 2021 05:32:21 +0000 Subject: [PATCH 79/87] Remove ladder --- cirq-core/cirq/ops/boolean_hamiltonian.py | 86 ++----------------- .../cirq/ops/boolean_hamiltonian_test.py | 61 +++++++------ .../json_test_data/BooleanHamiltonian.json | 3 +- .../json_test_data/BooleanHamiltonian.repr | 2 +- examples/qaoa.py | 2 +- 5 files changed, 40 insertions(+), 114 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 00e979f1ca9..c52dc5dbfe3 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -26,8 +26,7 @@ import functools import itertools -from typing import cast, Any, Callable, Dict, Generator, Iterator, List, Optional, Sequence, Tuple -import sys +from typing import cast, Any, Callable, Dict, Generator, List, Sequence, Tuple from sympy.logic.boolalg import And, Not, Or, Xor from sympy.core.expr import Expr @@ -54,7 +53,6 @@ def __init__( qubit_map: Dict[str, 'cirq.Qid'], boolean_strs: Sequence[str], theta: float, - ladder_target: Optional[bool] = None, ): """Builds a BooleanHamiltonian. @@ -75,40 +73,16 @@ def __init__( boolean_strs: The list of Sympy-parsable Boolean expressions. qubit_map: map of string (boolean variable name) to qubit. theta: The list of thetas to scale the Hamiltonian. - ladder_target: Whether to use convention of figure 7a or 7b of [4]. The two - formulations yield the same output, but can be simplified differently. - For example for 3 qubits, the ladder target would be: - ───@─────────── - │ - ───X───@─────── - │ - ───────X───@─── - │ - ───────────X─── - Otherwise, it would be: - ───@─────────── - │ - ───┼───@─────── - │ │ - ───┼───┼───@─── - │ │ │ - ───X───X───X─── - The papers do not provide a formula for which option yields a smaller number of - gates, so the option is provided as an input. - If set to None, computes the set of gates for both approaches, count the number of - gates, and return the smallest set. """ self._qubit_map: Dict[str, 'cirq.Qid'] = qubit_map self._boolean_strs: Sequence[str] = boolean_strs self._theta: float = theta - self._ladder_target: Optional[bool] = ladder_target def with_qubits(self, *new_qubits: 'cirq.Qid') -> 'BooleanHamiltonian': return BooleanHamiltonian( {cast(cirq.NamedQubit, q).name: q for q in new_qubits}, self._boolean_strs, self._theta, - self._ladder_target, ) @property @@ -119,7 +93,7 @@ def num_qubits(self) -> int: return len(self._qubit_map) def _value_equality_values_(self): - return self._qubit_map, self._boolean_strs, self._theta, self._ladder_target + return self._qubit_map, self._boolean_strs, self._theta def _json_dict_(self) -> Dict[str, Any]: return { @@ -127,12 +101,11 @@ def _json_dict_(self) -> Dict[str, Any]: 'qubit_map': self._qubit_map, 'boolean_strs': self._boolean_strs, 'theta': self._theta, - 'ladder_target': self._ladder_target, } @classmethod - def _from_json_dict_(cls, qubit_map, boolean_strs, theta, ladder_target, **kwargs): - return cls(qubit_map, boolean_strs, theta, ladder_target) + def _from_json_dict_(cls, qubit_map, boolean_strs, theta, **kwargs): + return cls(qubit_map, boolean_strs, theta) def _decompose_(self): boolean_exprs = [sympy_parser.parse_expr(boolean_str) for boolean_str in self._boolean_strs] @@ -141,15 +114,8 @@ def _decompose_(self): for boolean_expr in boolean_exprs ] - ladder_target_list = [True, False] if self._ladder_target is None else [self._ladder_target] - - return _shortest_generator( - [ - _get_gates_from_hamiltonians( - hamiltonian_polynomial_list, self._qubit_map, self._theta, self._ladder_target - ) - for ladder_target in ladder_target_list - ] + return _get_gates_from_hamiltonians( + hamiltonian_polynomial_list, self._qubit_map, self._theta ) @@ -363,7 +329,6 @@ def _get_gates_from_hamiltonians( hamiltonian_polynomial_list: List['cirq.PauliSum'], qubit_map: Dict[str, 'cirq.Qid'], theta: float, - ladder_target: bool = False, ) -> Generator['cirq.Operation', None, None]: """Builds a circuit according to [1]. @@ -372,25 +337,6 @@ def _get_gates_from_hamiltonians( _build_hamiltonian_from_boolean(). qubit_map: map of string (boolean variable name) to qubit. theta: A single float scaling the rotations. - ladder_target: Whether to use convention of figure 7a or 7b of [4]. The two formulations - yield the same output, but can be simplified differently. - For example for 3 qubits, the `ladder_target=True` results in: - ───@─────────── - │ - ───┼───@─────── - │ │ - ───┼───┼───@─── - │ │ │ - ───X───X───X─── - Otherwise, it would be: - ───@─────────── - │ - ───X───@─────── - │ - ───────X───@─── - │ - ───────────X─── - Yields: Gates that are the decomposition of the Hamiltonian. """ @@ -417,12 +363,8 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): cnots: List[Tuple[int, int]] = [] - if ladder_target: - cnots.extend((prevh[i], prevh[i + 1]) for i in reversed(range(len(prevh) - 1))) - cnots.extend((currh[i], currh[i + 1]) for i in range(len(currh) - 1)) - else: - 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)) + 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)) cnots = _simplify_cnots(cnots) @@ -442,15 +384,3 @@ def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): # Flush the last CNOTs. yield _apply_cnots(previous_h, ()) - - -def _shortest_generator( - generators: List[Generator['cirq.Operation', None, None]] -) -> Iterator['cirq.Operation']: - min_gen_length = sys.maxsize - for generator in generators: - out_gen, gen_copy = itertools.tee(generator) - gen_length = sum(1 for _ in gen_copy) - if gen_length < min_gen_length: - min_gen = out_gen - return min_gen diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index c83cfc14cc5..1f2d4fd5110 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -60,38 +60,35 @@ def test_unsupported_op(): @pytest.mark.parametrize( - 'boolean_str,ladder_target', - itertools.product( - [ - 'x0', - '~x0', - 'x0 ^ x1', - 'x0 & x1', - 'x0 | x1', - 'x0 & x1 & x2', - 'x0 & x1 & ~x2', - 'x0 & ~x1 & x2', - 'x0 & ~x1 & ~x2', - '~x0 & x1 & x2', - '~x0 & x1 & ~x2', - '~x0 & ~x1 & x2', - '~x0 & ~x1 & ~x2', - 'x0 ^ x1 ^ x2', - 'x0 | (x1 & x2)', - 'x0 & (x1 | x2)', - '(x0 ^ x1 ^ x2) | (x2 ^ x3 ^ x4)', - '(x0 ^ x2 ^ x4) | (x1 ^ x2 ^ x3)', - 'x0 & x1 & (x2 | x3)', - 'x0 & ~x2', - '~x0 & x2', - 'x2 & ~x0', - '~x2 & x0', - '(x2 | x1) ^ x0', - ], - [False, True, None], - ), + 'boolean_str', + [ + 'x0', + '~x0', + 'x0 ^ x1', + 'x0 & x1', + 'x0 | x1', + 'x0 & x1 & x2', + 'x0 & x1 & ~x2', + 'x0 & ~x1 & x2', + 'x0 & ~x1 & ~x2', + '~x0 & x1 & x2', + '~x0 & x1 & ~x2', + '~x0 & ~x1 & x2', + '~x0 & ~x1 & ~x2', + 'x0 ^ x1 ^ x2', + 'x0 | (x1 & x2)', + 'x0 & (x1 | x2)', + '(x0 ^ x1 ^ x2) | (x2 ^ x3 ^ x4)', + '(x0 ^ x2 ^ x4) | (x1 ^ x2 ^ x3)', + 'x0 & x1 & (x2 | x3)', + 'x0 & ~x2', + '~x0 & x2', + 'x2 & ~x0', + '~x2 & x0', + '(x2 | x1) ^ x0', + ], ) -def test_circuit(boolean_str, ladder_target): +def test_circuit(boolean_str): boolean_expr = sympy_parser.parse_expr(boolean_str) var_names = cirq.parameter_names(boolean_expr) qubits = [cirq.NamedQubit(name) for name in var_names] @@ -111,7 +108,7 @@ def test_circuit(boolean_str, ladder_target): circuit.append(cirq.H.on_each(*qubits)) hamiltonian_gate = cirq.BooleanHamiltonian( - {q.name: q for q in qubits}, [boolean_str], 0.1 * math.pi, ladder_target + {q.name: q for q in qubits}, [boolean_str], 0.1 * math.pi ) assert hamiltonian_gate.with_qubits(*qubits) == hamiltonian_gate diff --git a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json index c4f5017d725..1cd3ff32f84 100644 --- a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json +++ b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.json @@ -10,7 +10,6 @@ "boolean_strs": [ "q0" ], - "theta": 0.20160913, - "ladder_target": false + "theta": 0.20160913 } ] \ No newline at end of file diff --git a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr index a012efd2d2e..86756d03047 100644 --- a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr +++ b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonian.repr @@ -1 +1 @@ -[cirq.BooleanHamiltonian({'q0': cirq.NamedQubit('q0')}, ['q0'], 0.20160913, False)] \ No newline at end of file +[cirq.BooleanHamiltonian({'q0': cirq.NamedQubit('q0')}, ['q0'], 0.20160913)] \ No newline at end of file diff --git a/examples/qaoa.py b/examples/qaoa.py index 41e99b9af1d..b96f04d3265 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -116,7 +116,7 @@ def f(x): for i in range(p): hamiltonian_gate = cirq.BooleanHamiltonian( - {q.name: q for q in qubits}, booleans, 2.0 * x[p + i], ladder_target=True + {q.name: q for q in qubits}, booleans, 2.0 * x[p + i] ) circuit.append(hamiltonian_gate) circuit.append(cirq.rx(2.0 * x[i]).on_each(*qubits)) From d2e9ce88543c8944ff6191976c4313ce026300e7 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 9 Jul 2021 21:52:32 +0000 Subject: [PATCH 80/87] Merge changes --- cirq-core/cirq/ops/boolean_hamiltonian.py | 84 +++---------------- .../cirq/ops/boolean_hamiltonian_test.py | 35 -------- 2 files changed, 10 insertions(+), 109 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index c52dc5dbfe3..beee97f1ae8 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -20,28 +20,16 @@ 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, Alán Aspuru-Guzik, https://arxiv.org/abs/1306.3991 """ -import functools -import itertools -from typing import cast, Any, Callable, Dict, Generator, List, Sequence, Tuple +from typing import cast, Any, Dict, Generator, List, Sequence, Tuple -from sympy.logic.boolalg import And, Not, Or, Xor -from sympy.core.expr import Expr -from sympy.core.symbol import Symbol import sympy.parsing.sympy_parser as sympy_parser import cirq from cirq import value from cirq.ops import raw_types - -_hamiltonian_O: Callable[[], 'cirq.PauliString'] = lambda: 0.0 * cirq.PauliString({}) -_hamiltonian_I: Callable[[], 'cirq.PauliString'] = lambda: cirq.PauliString({}) -_hamiltonian_Z: Callable[['cirq.Qid'], 'cirq.PauliString'] = lambda qubit: cirq.PauliString( - {qubit: cirq.Z} -) +from cirq.ops.linear_combinations import PauliSum, PauliString @value.value_equality @@ -110,7 +98,7 @@ def _from_json_dict_(cls, qubit_map, boolean_strs, theta, **kwargs): def _decompose_(self): boolean_exprs = [sympy_parser.parse_expr(boolean_str) for boolean_str in self._boolean_strs] hamiltonian_polynomial_list = [ - _build_hamiltonian_from_boolean(boolean_expr, self._qubit_map) + PauliSum.from_boolean_expression(boolean_expr, self._qubit_map) for boolean_expr in boolean_exprs ] @@ -119,51 +107,6 @@ def _decompose_(self): ) -def _build_hamiltonian_from_boolean( - boolean_expr: Expr, qubit_map: Dict[str, 'cirq.Qid'] -) -> 'cirq.PauliString': - """Builds the Hamiltonian representation of Boolean expression as per [1]: - - We can represent any Boolean expression as a polynomial of Pauli Zs on different qubits. For - example, this object could represent the polynomial 0.5*I - 0.5*Z_1*Z_2. - - Args: - boolean_expr: A Sympy expression containing symbols and Boolean operations - qubit_map: map of string (boolean variable name) to qubit. - - Return: - The PauliString that represents the Boolean expression. - """ - if isinstance(boolean_expr, Symbol): - # Table 1 of [1], entry for 'x' is '1/2.I - 1/2.Z' - return 0.5 * _hamiltonian_I() - 0.5 * _hamiltonian_Z(qubit_map[boolean_expr.name]) - - if isinstance(boolean_expr, (And, Not, Or, Xor)): - sub_hamiltonians = [ - _build_hamiltonian_from_boolean(sub_boolean_expr, qubit_map) - for sub_boolean_expr in boolean_expr.args - ] - # We apply the equalities of theorem 1 of [1]. - if isinstance(boolean_expr, And): - hamiltonian = _hamiltonian_I() - for sub_hamiltonian in sub_hamiltonians: - hamiltonian = hamiltonian * sub_hamiltonian - elif isinstance(boolean_expr, Not): - assert len(sub_hamiltonians) == 1 - hamiltonian = _hamiltonian_I() - sub_hamiltonians[0] - elif isinstance(boolean_expr, Or): - hamiltonian = _hamiltonian_O() - for sub_hamiltonian in sub_hamiltonians: - hamiltonian = hamiltonian + sub_hamiltonian - hamiltonian * sub_hamiltonian - elif isinstance(boolean_expr, Xor): - hamiltonian = _hamiltonian_O() - for sub_hamiltonian in sub_hamiltonians: - hamiltonian = hamiltonian + sub_hamiltonian - 2.0 * hamiltonian * sub_hamiltonian - return hamiltonian - - raise ValueError(f'Unsupported type: {type(boolean_expr)}') - - def _gray_code_comparator(k1: Tuple[int, ...], k2: Tuple[int, ...], flip: bool = False) -> int: """Compares two Gray-encoded binary numbers. @@ -334,13 +277,13 @@ def _get_gates_from_hamiltonians( Args: hamiltonian_polynomial_list: the list of Hamiltonians, typically built by calling - _build_hamiltonian_from_boolean(). + PauliSum.from_boolean_expression(). qubit_map: map of string (boolean variable name) to qubit. theta: A single float scaling the rotations. Yields: Gates that are the decomposition of the Hamiltonian. """ - combined = sum(hamiltonian_polynomial_list, _hamiltonian_O()) + combined = sum(hamiltonian_polynomial_list, PauliSum.from_pauli_strings(PauliString({}))) qubit_names = sorted(qubit_map.keys()) qubits = [qubit_map[name] for name in qubit_names] @@ -352,29 +295,22 @@ def _get_gates_from_hamiltonians( qubit_idx = tuple(sorted(qubit_indices[qubit] for qubit in pauli_string.qubits)) hamiltonians[qubit_idx] = w - # Here we follow improvements of [4] cancelling out the CNOTs. The first step is to order by - # Gray code so that as few as possible gates are changed. - sorted_hs = sorted(list(hamiltonians.keys()), key=functools.cmp_to_key(_gray_code_comparator)) - def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]): - # This function applies in sequence the CNOTs from prevh and then currh. However, given - # that the h are sorted in Gray ordering and that some cancel each other, we can reduce - # the number of gates. See [4] for more details. - cnots: List[Tuple[int, 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)) - cnots = _simplify_cnots(cnots) + # 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 for gate in (cirq.CNOT(qubits[c], qubits[t]) for c, t in cnots): yield gate previous_h: Tuple[int, ...] = () - for h in sorted_hs: - w = hamiltonians[h] - + for h, w in hamiltonians.items(): 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 1f2d4fd5110..d1328aeaf41 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -23,41 +23,6 @@ import cirq import cirq.ops.boolean_hamiltonian as bh -# These are some of the entries of table 1 of https://arxiv.org/pdf/1804.09130.pdf. -@pytest.mark.parametrize( - 'boolean_expr,expected_hamiltonian_polynomial', - [ - ('x', ['(-0.5+0j)*Z(x)', '(0.5+0j)*I']), - ('~x', ['(0.5+0j)*I', '(0.5+0j)*Z(x)']), - ('x0 ^ x1', ['(-0.5+0j)*Z(x0)*Z(x1)', '(0.5+0j)*I']), - ( - 'x0 & x1', - ['(-0.25+0j)*Z(x0)', '(-0.25+0j)*Z(x1)', '(0.25+0j)*I', '(0.25+0j)*Z(x0)*Z(x1)'], - ), - ( - 'x0 | x1', - ['(-0.25+0j)*Z(x0)', '(-0.25+0j)*Z(x0)*Z(x1)', '(-0.25+0j)*Z(x1)', '(0.75+0j)*I'], - ), - ('x0 ^ x1 ^ x2', ['(-0.5+0j)*Z(x0)*Z(x1)*Z(x2)', '(0.5+0j)*I']), - ], -) -def test_build_hamiltonian_from_boolean(boolean_expr, expected_hamiltonian_polynomial): - boolean = sympy_parser.parse_expr(boolean_expr) - qubit_map = {name: cirq.NamedQubit(name) for name in sorted(cirq.parameter_names(boolean))} - actual = bh._build_hamiltonian_from_boolean(boolean, qubit_map) - # Instead of calling str() directly, first make sure that the items are sorted. This is to make - # the unit test more robut in case Sympy would result in a different parsing order. By sorting - # the individual items, we would have a canonical representation. - actual_items = list(sorted(str(pauli_string) for pauli_string in actual)) - assert expected_hamiltonian_polynomial == actual_items - - -def test_unsupported_op(): - not_a_boolean = sympy_parser.parse_expr('x * x') - qubit_map = {name: cirq.NamedQubit(name) for name in cirq.parameter_names(not_a_boolean)} - with pytest.raises(ValueError, match='Unsupported type'): - bh._build_hamiltonian_from_boolean(not_a_boolean, qubit_map) - @pytest.mark.parametrize( 'boolean_str', From e93b47267a378eab6187044ca5692cd1605a0d6c Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 9 Jul 2021 22:10:44 +0000 Subject: [PATCH 81/87] Actually call simplification code --- cirq-core/cirq/ops/boolean_hamiltonian.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index beee97f1ae8..27e4ed05167 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -21,7 +21,7 @@ [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 from typing import cast, Any, Dict, Generator, List, Sequence, Tuple import sympy.parsing.sympy_parser as sympy_parser @@ -301,10 +301,7 @@ 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 From 94eb92daa8bf5d97f8c58be52fcf6d77f581e751 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Thu, 15 Jul 2021 04:15:06 +0000 Subject: [PATCH 82/87] nit --- cirq-core/cirq/ops/boolean_hamiltonian.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 27e4ed05167..c06eb1601a8 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -11,8 +11,7 @@ # 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. -""" -Represents Boolean functions as a series of CNOT and rotation gates. The Boolean functions are +"""Represents Boolean functions as a series of CNOT and rotation gates. The Boolean functions are passed as Sympy expressions and then turned into an optimized set of gates. References: From f3149a1ff277717d87a619d0c3db40d86804ed2b Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Fri, 6 Aug 2021 03:45:11 +0000 Subject: [PATCH 83/87] Remove boolean hamiltonian --- cirq-core/cirq/ops/boolean_hamiltonian.py | 171 ------------------ .../cirq/ops/boolean_hamiltonian_test.py | 99 ---------- 2 files changed, 270 deletions(-) diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index c7d5a94168a..c5a3de0776b 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -20,7 +20,6 @@ [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 from typing import cast, Any, Dict, Generator, List, Sequence, Tuple @@ -49,12 +48,6 @@ def __init__( polynomials, thus making a function that goes from a series to Boolean inputs to an integer that is the number of Boolean expressions that are true. - For example, if we were using this gate for the max-cut problem that is typically used to - demonstrate the QAOA algorithm, there would be one Boolean expression per edge. Each - Boolean expression would be true iff the vertices on that are in different cuts (i.e. it's) - an XOR. - - Then, we compute exp(j * theta * polynomial), which is unitary because the polynomial is For example, if we were using this gate for the unweighted max-cut problem that is typically used to demonstrate the QAOA algorithm, there would be one Boolean expression per edge. Each Boolean expression would be true iff the vertices on that are in different cuts (i.e. it's) @@ -66,7 +59,6 @@ def __init__( Args: boolean_strs: The list of Sympy-parsable Boolean expressions. qubit_map: map of string (boolean variable name) to qubit. - theta: The list of thetas to scale the Hamiltonian. theta: The evolution time (angle) for the Hamiltonian """ self._qubit_map: Dict[str, 'cirq.Qid'] = qubit_map @@ -114,168 +106,6 @@ 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. - - 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'], @@ -309,7 +139,6 @@ 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)) - cnots = _simplify_cnots(cnots) # 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 diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index a3066b26d0e..c23a01ad7b9 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -11,10 +11,6 @@ # 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 itertools import math @@ -23,8 +19,6 @@ import sympy.parsing.sympy_parser as sympy_parser import cirq -import cirq.ops.boolean_hamiltonian as bh - @pytest.mark.parametrize( @@ -89,96 +83,3 @@ 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 15420695d5c08a6d910458e278e154385cff4413 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 14 Aug 2021 03:55:36 +0000 Subject: [PATCH 84/87] Obliterate diffs so that I can start over --- examples/examples_test.py | 2 +- examples/qaoa.py | 249 ++++++++++++++++++-------------------- 2 files changed, 122 insertions(+), 129 deletions(-) diff --git a/examples/examples_test.py b/examples/examples_test.py index d3e77f99f38..0ffc2e57e16 100644 --- a/examples/examples_test.py +++ b/examples/examples_test.py @@ -97,7 +97,7 @@ def test_example_heatmaps(): def test_example_runs_qaoa(): - examples.qaoa.main(repetitions=1, maxiter=1, p=1) + examples.qaoa.main(repetitions=10, maxiter=5) def test_example_runs_quantum_teleportation(): diff --git a/examples/qaoa.py b/examples/qaoa.py index b96f04d3265..87528dc25ac 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -1,162 +1,155 @@ """Runs the Quantum Approximate Optimization Algorithm on Max-Cut. -=== EXAMPLE CIRCUIT === - x0 x1 x2 x3 - │ │ │ │ - H H H H - │ │ │ │ - │ │ Y^-0.5 Y^-0.5 - │ │ │ │ - │ @─────@ │ - │ │ │ │ - │ │ Y^0.5 │ - │ │ │ │ - │ │ Rz(0) │ - │ │ │ │ - │ │ Y^-0.5 │ - │ │ │ │ - │ @─────@ │ - │ │ │ │ -┌╴│ │ │ │ ╶┐ -│ │ │ Y^0.5 │ │ -│ │ @─────┼──────@ │ -└╴│ │ │ │ ╶┘ - │ │ │ │ - │ │ Y^-0.5 Y^0.5 - │ │ │ │ - @─────┼─────@ Rz(0) - │ │ │ │ - │ │ Y^0.5 Y^-0.5 - │ │ │ │ -┌╴│ │ │ │ ╶┐ -│ │ │ Rz(0) │ │ -│ │ @─────┼──────@ │ -└╴│ │ │ │ ╶┘ - │ │ │ │ - │ Rx(0) Y^-0.5 Y^0.5 - │ │ │ │ - @─────┼─────@ Y^-0.5 - │ │ │ │ -┌╴│ │ │ │ ╶┐ -│ │ │ Y^0.5 │ │ -│ @─────┼─────┼──────@ │ -└╴│ │ │ │ ╶┘ - │ │ │ │ - │ │ Rx(0) Y^0.5 - │ │ │ │ - │ │ │ Rz(0) - │ │ │ │ - │ │ │ Y^-0.5 - │ │ │ │ - @─────┼─────┼──────@ - │ │ │ │ - Rx(0) │ │ Y^0.5 - │ │ │ │ - │ │ │ Rx(0) - │ │ │ │ - M─────M─────M──────M('m') - │ │ │ │ - === EXAMPLE OUTPUT === -Brute force max cut: 16 -μ=7.80 max=12 -μ=7.40 max=8 -μ=8.60 max=10 -μ=8.00 max=10 -μ=8.80 max=12 -μ=8.20 max=12 -μ=10.00 max=16 -μ=8.00 max=16 -μ=4.40 max=12 -μ=5.80 max=12 - - Return from subroutine COBYLA because the MAXFUN limit has been reached. - - NFVALS = 10 F =-1.000000E+01 MAXCV = 0.000000E+00 - X = 0.000000E+00 1.000000E+00 0.000000E+00 1.000000E+00 0.000000E+00 - 1.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 + +Example QAOA circuit: + 0 1 2 3 4 5 + │ │ │ │ │ │ + H H H H H H + │ │ │ │ │ │ + ZZ──────────ZZ^(-4/13) │ │ │ │ +┌ │ │ │ │ │ │ ┐ +│ ZZ──────────┼───────────ZZ^(-4/13) │ │ │ │ +│ │ ZZ──────────┼───────────ZZ^(-4/13) │ │ │ +└ │ │ │ │ │ │ ┘ +┌ │ │ │ │ │ │ ┐ +│ ZZ──────────┼───────────┼───────────┼───────────ZZ^(-4/13) │ │ +│ │ ZZ──────────┼───────────┼───────────┼───────────ZZ^(-4/13) │ +└ │ │ │ │ │ │ ┘ + Rx(0.151π) Rx(0.151π) ZZ──────────┼───────────ZZ^(-4/13) │ + │ │ │ │ │ │ + ZZ──────────ZZ^-0.941 ZZ──────────┼───────────┼───────────ZZ^(-4/13) + │ │ │ ZZ──────────ZZ^(-4/13) │ +┌ │ │ │ │ │ │ ┐ +│ │ │ Rx(0.151π) ZZ──────────┼───────────ZZ^(-4/13) │ +│ │ │ │ │ Rx(0.151π) │ │ +└ │ │ │ │ │ │ ┘ + ZZ──────────┼───────────ZZ^-0.941 Rx(0.151π) │ Rx(0.151π) +┌ │ │ │ │ │ │ ┐ +│ ZZ──────────┼───────────┼───────────┼───────────ZZ^-0.941 │ │ +│ │ ZZ──────────┼───────────ZZ^-0.941 │ │ │ +└ │ │ │ │ │ │ ┘ + Rx(-0.448π) ZZ──────────┼───────────┼───────────┼───────────ZZ^-0.941 + │ │ ZZ──────────┼───────────ZZ^-0.941 │ + │ │ │ │ │ │ + │ Rx(-0.448π) ZZ──────────┼───────────┼───────────ZZ^-0.941 + │ │ │ ZZ──────────ZZ^-0.941 │ +┌ │ │ │ │ │ │ ┐ +│ │ │ Rx(-0.448π) ZZ──────────┼───────────ZZ^-0.941 │ +│ │ │ │ │ Rx(-0.448π) │ │ +└ │ │ │ │ │ │ ┘ + │ │ │ Rx(-0.448π) │ Rx(-0.448π) + │ │ │ │ │ │ + M('m')──────M───────────M───────────M───────────M───────────M + │ │ │ │ │ │ +Optimizing objective function ... +The largest cut value found was 7. +The largest possible cut has size 7. +The approximation ratio achieved is 1.0. """ + import itertools -from typing import List import numpy as np import networkx import scipy.optimize -from sympy.parsing.sympy_parser import parse_expr import cirq -def brute_force(graph, n): - bitstrings = np.array(list(itertools.product(range(2), repeat=n))) - mat = networkx.adjacency_matrix(graph, nodelist=sorted(graph.nodes)) - vecs = (-1) ** bitstrings - vals = 0.5 * np.sum(vecs * (mat @ vecs.T).T, axis=-1) - vals = 0.5 * (graph.size() - vals) - return max(np.round(vals)) +def main(repetitions=1000, maxiter=50): + # Set problem parameters + n = 6 + p = 2 + # Generate a random 3-regular graph on n nodes + graph = networkx.random_regular_graph(3, n) -def qaoa(booleans: List[str], repetitions: int, maxiter: int, p: int): - """Run the QAOA optimization for a list of Boolean expressions. + # Make qubits + qubits = cirq.LineQubit.range(n) - Args: - booleans: A list of Boolean expressions (we want as many of them to be true as possible). - repetitions: The number of times to repeat the measurements. - maxiter: The number of iterations of the optimizer. - p: The number of times to repeat the Hamiltonian gate. - """ - boolean_exprs = [parse_expr(boolean) for boolean in booleans] - param_names = cirq.parameter_names(boolean_exprs) - qubits = [cirq.NamedQubit(name) for name in param_names] + # Print an example circuit + betas = np.random.uniform(-np.pi, np.pi, size=p) + gammas = np.random.uniform(-np.pi, np.pi, size=p) + circuit = qaoa_max_cut_circuit(qubits, betas, gammas, graph) + print('Example QAOA circuit:') + print(circuit.to_text_diagram(transpose=True)) - def f(x): - # Build the circuit. - circuit = cirq.Circuit() - circuit.append(cirq.H.on_each(*qubits)) + # Create variables to store the largest cut and cut value found + largest_cut_found = None + largest_cut_value_found = 0 - for i in range(p): - hamiltonian_gate = cirq.BooleanHamiltonian( - {q.name: q for q in qubits}, booleans, 2.0 * x[p + i] - ) - circuit.append(hamiltonian_gate) - circuit.append(cirq.rx(2.0 * x[i]).on_each(*qubits)) + # Initialize simulator + simulator = cirq.Simulator() - circuit.append(cirq.measure(*qubits, key='m')) + # Define objective function (we'll use the negative expected cut value) - # Measure - result = cirq.Simulator().run(circuit, repetitions=repetitions) + def f(x): + # Create circuit + betas = x[:p] + gammas = x[p:] + circuit = qaoa_max_cut_circuit(qubits, betas, gammas, graph) + # Sample bitstrings from circuit + result = simulator.run(circuit, repetitions=repetitions) bitstrings = result.measurements['m'] + # Process bitstrings + nonlocal largest_cut_found + nonlocal largest_cut_value_found + values = cut_values(bitstrings, graph) + max_value_index = np.argmax(values) + max_value = values[max_value_index] + if max_value > largest_cut_value_found: + largest_cut_value_found = max_value + largest_cut_found = bitstrings[max_value_index] + mean = np.mean(values) + return -mean + + # Pick an initial guess + x0 = np.random.uniform(-np.pi, np.pi, size=2 * p) + + # Optimize f + print('Optimizing objective function ...') + scipy.optimize.minimize(f, x0, method='Nelder-Mead', options={'maxiter': maxiter}) - # Evaluate - values = [] - for rep in range(repetitions): - subs = {name: val == 1 for name, val in zip(param_names, bitstrings[rep, :])} - values.append( - sum(1 if boolean_expr.subs(subs) else 0 for boolean_expr in boolean_exprs) - ) + # Compute best possible cut value via brute force search + all_bitstrings = np.array(list(itertools.product(range(2), repeat=n))) + all_values = cut_values(all_bitstrings, graph) + max_cut_value = np.max(all_values) - print('μ=%.2f max=%d' % (np.mean(values), max(values))) + # Print the results + print(f'The largest cut value found was {largest_cut_value_found}.') + print(f'The largest possible cut has size {max_cut_value}.') + print(f'The approximation ratio achieved is {largest_cut_value_found / max_cut_value}.') - return -np.mean(values) - x0 = np.zeros(2 * p) - scipy.optimize.minimize(f, x0, method='COBYLA', options={'maxiter': maxiter, 'disp': True}) +def rzz(rads): + """Returns a gate with the matrix exp(-i Z⊗Z rads).""" + return cirq.ZZPowGate(exponent=2 * rads / np.pi, global_shift=-0.5) -def main(repetitions=10, maxiter=250, p=5): - # Set problem parameters - n = 6 +def qaoa_max_cut_unitary(qubits, betas, gammas, graph): # Nodes should be integers + for beta, gamma in zip(betas, gammas): + yield (rzz(-0.5 * gamma).on(qubits[i], qubits[j]) for i, j in graph.edges) + yield cirq.rx(2 * beta).on_each(*qubits) - # Generate a random bipartite graph. - graph = networkx.complete_multipartite_graph(n, n) - # Compute best possible cut value via brute force search - print('Brute force max cut: %d' % (brute_force(graph, 2 * n))) +def qaoa_max_cut_circuit(qubits, betas, gammas, graph): # Nodes should be integers + return cirq.Circuit( + # Prepare uniform superposition + cirq.H.on_each(*qubits), + # Apply QAOA unitary + qaoa_max_cut_unitary(qubits, betas, gammas, graph), + # Measure + cirq.measure(*qubits, key='m'), + ) - # Build the boolean expressions - booleans = [f"x{i} ^ x{j}" for i, j in graph.edges] - qaoa(booleans, repetitions=repetitions, maxiter=maxiter, p=p) +def cut_values(bitstrings, graph): + mat = networkx.adjacency_matrix(graph, nodelist=sorted(graph.nodes)) + vecs = (-1) ** bitstrings + vals = 0.5 * np.sum(vecs * (mat @ vecs.T).T, axis=-1) + vals = 0.5 * (graph.size() - vals) + return vals if __name__ == '__main__': From 277850fc56217ea7903a8d4d04a89b647292c213 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 14 Aug 2021 12:59:46 +0000 Subject: [PATCH 85/87] Refactor to use both approaches --- examples/examples_test.py | 3 ++- examples/qaoa.py | 39 +++++++++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/examples/examples_test.py b/examples/examples_test.py index 0ffc2e57e16..539d2687bc8 100644 --- a/examples/examples_test.py +++ b/examples/examples_test.py @@ -97,7 +97,8 @@ def test_example_heatmaps(): def test_example_runs_qaoa(): - examples.qaoa.main(repetitions=10, maxiter=5) + examples.qaoa.main(repetitions=10, maxiter=5, use_boolean_hamiltonian_gate=False) + examples.qaoa.main(repetitions=10, maxiter=5, use_boolean_hamiltonian_gate=True) def test_example_runs_quantum_teleportation(): diff --git a/examples/qaoa.py b/examples/qaoa.py index 87528dc25ac..ac7078f6f50 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -53,11 +53,12 @@ import numpy as np import networkx import scipy.optimize +from sympy.parsing.sympy_parser import parse_expr import cirq -def main(repetitions=1000, maxiter=50): +def main(repetitions=10, maxiter=250, use_boolean_hamiltonian_gate=True, method=None): # Set problem parameters n = 6 p = 2 @@ -71,7 +72,7 @@ def main(repetitions=1000, maxiter=50): # Print an example circuit betas = np.random.uniform(-np.pi, np.pi, size=p) gammas = np.random.uniform(-np.pi, np.pi, size=p) - circuit = qaoa_max_cut_circuit(qubits, betas, gammas, graph) + circuit = qaoa_max_cut_circuit(qubits, betas, gammas, graph, use_boolean_hamiltonian_gate) print('Example QAOA circuit:') print(circuit.to_text_diagram(transpose=True)) @@ -88,7 +89,7 @@ def f(x): # Create circuit betas = x[:p] gammas = x[p:] - circuit = qaoa_max_cut_circuit(qubits, betas, gammas, graph) + circuit = qaoa_max_cut_circuit(qubits, betas, gammas, graph, use_boolean_hamiltonian_gate) # Sample bitstrings from circuit result = simulator.run(circuit, repetitions=repetitions) bitstrings = result.measurements['m'] @@ -127,21 +128,35 @@ def rzz(rads): return cirq.ZZPowGate(exponent=2 * rads / np.pi, global_shift=-0.5) -def qaoa_max_cut_unitary(qubits, betas, gammas, graph): # Nodes should be integers - for beta, gamma in zip(betas, gammas): - yield (rzz(-0.5 * gamma).on(qubits[i], qubits[j]) for i, j in graph.edges) - yield cirq.rx(2 * beta).on_each(*qubits) - - -def qaoa_max_cut_circuit(qubits, betas, gammas, graph): # Nodes should be integers - return cirq.Circuit( +def qaoa_max_cut_unitary( + qubits, betas, gammas, graph, use_boolean_hamiltonian_gate +): # Nodes should be integers + if use_boolean_hamiltonian_gate: + booleans = [f"x{i} ^ x{j}" for i, j in sorted(graph.edges)] + param_names = [f"x{i}" for i in range(len(qubits))] + qubit_map = {param_name: qubit for param_name, qubit in zip(param_names, qubits)} + for beta, gamma in zip(betas, gammas): + yield cirq.BooleanHamiltonian(qubit_map, booleans, 2.0 * gamma) + yield cirq.rx(2 * beta).on_each(*qubits) + else: + for beta, gamma in zip(betas, gammas): + yield (rzz(-0.5 * gamma).on(qubits[i], qubits[j]) for i, j in graph.edges) + yield cirq.rx(2 * beta).on_each(*qubits) + + +def qaoa_max_cut_circuit( + qubits, betas, gammas, graph, use_boolean_hamiltonian_gate +): # Nodes should be integers + circuit = cirq.Circuit( # Prepare uniform superposition cirq.H.on_each(*qubits), # Apply QAOA unitary - qaoa_max_cut_unitary(qubits, betas, gammas, graph), + qaoa_max_cut_unitary(qubits, betas, gammas, graph, use_boolean_hamiltonian_gate), # Measure cirq.measure(*qubits, key='m'), ) + cirq.DropNegligible().optimize_circuit(circuit) + return circuit def cut_values(bitstrings, graph): From 66ec92138bd309f78315a2fa830c5842450b27fc Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 14 Aug 2021 13:06:03 +0000 Subject: [PATCH 86/87] Nits --- examples/qaoa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/qaoa.py b/examples/qaoa.py index ac7078f6f50..764e31c02e3 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -58,7 +58,7 @@ import cirq -def main(repetitions=10, maxiter=250, use_boolean_hamiltonian_gate=True, method=None): +def main(repetitions=10, maxiter=50, use_boolean_hamiltonian_gate=False): # Set problem parameters n = 6 p = 2 From d33bbe37d8cf9304d597d91b08e1aba5203517d1 Mon Sep 17 00:00:00 2001 From: Tony Bruguier Date: Sat, 14 Aug 2021 13:12:54 +0000 Subject: [PATCH 87/87] nit --- examples/qaoa.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/qaoa.py b/examples/qaoa.py index 764e31c02e3..79977289ac4 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -53,7 +53,6 @@ import numpy as np import networkx import scipy.optimize -from sympy.parsing.sympy_parser import parse_expr import cirq