Skip to content

Commit 0fdefac

Browse files
authored
Boolean Hamiltonian gate (quantumlib#4309)
Following quantumlib#4282, the present PR would like to add a gate that allows computing the Hamiltonian from a Boolean expression. Note that while the decomposition is somewhat efficient, more optimizations are possible and planned in a follow-up PR.
1 parent 136096c commit 0fdefac

7 files changed

+267
-0
lines changed

cirq/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
BaseDensePauliString,
176176
bit_flip,
177177
BitFlipChannel,
178+
BooleanHamiltonian,
178179
CCX,
179180
CCXPowGate,
180181
CCZ,

cirq/json_resolver_cache.py

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def two_qubit_matrix_gate(matrix):
5252
'AsymmetricDepolarizingChannel': cirq.AsymmetricDepolarizingChannel,
5353
'BitFlipChannel': cirq.BitFlipChannel,
5454
'BitstringAccumulator': cirq.work.BitstringAccumulator,
55+
'BooleanHamiltonian': cirq.BooleanHamiltonian,
5556
'ProductState': cirq.ProductState,
5657
'CCNotPowGate': cirq.CCNotPowGate,
5758
'CCXPowGate': cirq.CCXPowGate,

cirq/ops/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
ArithmeticOperation,
1919
)
2020

21+
from cirq.ops.boolean_hamiltonian import (
22+
BooleanHamiltonian,
23+
)
24+
2125
from cirq.ops.clifford_gate import (
2226
PauliTransform,
2327
SingleQubitCliffordGate,

cirq/ops/boolean_hamiltonian.py

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Copyright 2021 The Cirq Developers
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Represents Boolean functions as a series of CNOT and rotation gates. The Boolean functions are
15+
passed as Sympy expressions and then turned into an optimized set of gates.
16+
17+
References:
18+
[1] On the representation of Boolean and real functions as Hamiltonians for quantum computing
19+
by Stuart Hadfield, https://arxiv.org/pdf/1804.09130.pdf
20+
[2] https://www.youtube.com/watch?v=AOKM9BkweVU is a useful intro
21+
[3] https://github.com/rsln-s/IEEE_QW_2020/blob/master/Slides.pdf
22+
"""
23+
24+
from typing import cast, Any, Dict, Generator, List, Sequence, Tuple
25+
26+
import sympy.parsing.sympy_parser as sympy_parser
27+
28+
import cirq
29+
from cirq import value
30+
from cirq.ops import raw_types
31+
from cirq.ops.linear_combinations import PauliSum, PauliString
32+
33+
34+
@value.value_equality
35+
class BooleanHamiltonian(raw_types.Operation):
36+
"""An operation that represents a Hamiltonian from a set of Boolean functions."""
37+
38+
def __init__(
39+
self,
40+
qubit_map: Dict[str, 'cirq.Qid'],
41+
boolean_strs: Sequence[str],
42+
theta: float,
43+
):
44+
"""Builds a BooleanHamiltonian.
45+
46+
For each element of a sequence of Boolean expressions, the code first transforms it into a
47+
polynomial of Pauli Zs that represent that particular expression. Then, we sum all the
48+
polynomials, thus making a function that goes from a series to Boolean inputs to an integer
49+
that is the number of Boolean expressions that are true.
50+
51+
For example, if we were using this gate for the unweighted max-cut problem that is typically
52+
used to demonstrate the QAOA algorithm, there would be one Boolean expression per edge. Each
53+
Boolean expression would be true iff the vertices on that are in different cuts (i.e. it's)
54+
an XOR.
55+
56+
Then, we compute exp(-j * theta * polynomial), which is unitary because the polynomial is
57+
Hermitian.
58+
59+
Args:
60+
boolean_strs: The list of Sympy-parsable Boolean expressions.
61+
qubit_map: map of string (boolean variable name) to qubit.
62+
theta: The evolution time (angle) for the Hamiltonian
63+
"""
64+
self._qubit_map: Dict[str, 'cirq.Qid'] = qubit_map
65+
self._boolean_strs: Sequence[str] = boolean_strs
66+
self._theta: float = theta
67+
68+
def with_qubits(self, *new_qubits: 'cirq.Qid') -> 'BooleanHamiltonian':
69+
return BooleanHamiltonian(
70+
{cast(cirq.NamedQubit, q).name: q for q in new_qubits},
71+
self._boolean_strs,
72+
self._theta,
73+
)
74+
75+
@property
76+
def qubits(self) -> Tuple[raw_types.Qid, ...]:
77+
return tuple(self._qubit_map.values())
78+
79+
def num_qubits(self) -> int:
80+
return len(self._qubit_map)
81+
82+
def _value_equality_values_(self):
83+
return self._qubit_map, self._boolean_strs, self._theta
84+
85+
def _json_dict_(self) -> Dict[str, Any]:
86+
return {
87+
'cirq_type': self.__class__.__name__,
88+
'qubit_map': self._qubit_map,
89+
'boolean_strs': self._boolean_strs,
90+
'theta': self._theta,
91+
}
92+
93+
@classmethod
94+
def _from_json_dict_(cls, qubit_map, boolean_strs, theta, **kwargs):
95+
return cls(qubit_map, boolean_strs, theta)
96+
97+
def _decompose_(self):
98+
boolean_exprs = [sympy_parser.parse_expr(boolean_str) for boolean_str in self._boolean_strs]
99+
hamiltonian_polynomial_list = [
100+
PauliSum.from_boolean_expression(boolean_expr, self._qubit_map)
101+
for boolean_expr in boolean_exprs
102+
]
103+
104+
return _get_gates_from_hamiltonians(
105+
hamiltonian_polynomial_list, self._qubit_map, self._theta
106+
)
107+
108+
109+
def _get_gates_from_hamiltonians(
110+
hamiltonian_polynomial_list: List['cirq.PauliSum'],
111+
qubit_map: Dict[str, 'cirq.Qid'],
112+
theta: float,
113+
) -> Generator['cirq.Operation', None, None]:
114+
"""Builds a circuit according to [1].
115+
116+
Args:
117+
hamiltonian_polynomial_list: the list of Hamiltonians, typically built by calling
118+
PauliSum.from_boolean_expression().
119+
qubit_map: map of string (boolean variable name) to qubit.
120+
theta: A single float scaling the rotations.
121+
Yields:
122+
Gates that are the decomposition of the Hamiltonian.
123+
"""
124+
combined = sum(hamiltonian_polynomial_list, PauliSum.from_pauli_strings(PauliString({})))
125+
126+
qubit_names = sorted(qubit_map.keys())
127+
qubits = [qubit_map[name] for name in qubit_names]
128+
qubit_indices = {qubit: i for i, qubit in enumerate(qubits)}
129+
130+
hamiltonians = {}
131+
for pauli_string in combined:
132+
w = pauli_string.coefficient.real
133+
qubit_idx = tuple(sorted(qubit_indices[qubit] for qubit in pauli_string.qubits))
134+
hamiltonians[qubit_idx] = w
135+
136+
def _apply_cnots(prevh: Tuple[int, ...], currh: Tuple[int, ...]):
137+
cnots: List[Tuple[int, int]] = []
138+
139+
cnots.extend((prevh[i], prevh[-1]) for i in range(len(prevh) - 1))
140+
cnots.extend((currh[i], currh[-1]) for i in range(len(currh) - 1))
141+
142+
# TODO(tonybruguier): At this point, some CNOT gates can be cancelled out according to:
143+
# "Efficient quantum circuits for diagonal unitaries without ancillas" by Jonathan Welch,
144+
# Daniel Greenbaum, Sarah Mostame, Alán Aspuru-Guzik
145+
# https://arxiv.org/abs/1306.3991
146+
147+
for gate in (cirq.CNOT(qubits[c], qubits[t]) for c, t in cnots):
148+
yield gate
149+
150+
previous_h: Tuple[int, ...] = ()
151+
for h, w in hamiltonians.items():
152+
yield _apply_cnots(previous_h, h)
153+
154+
if len(h) >= 1:
155+
yield cirq.Rz(rads=(theta * w)).on(qubits[h[-1]])
156+
157+
previous_h = h
158+
159+
# Flush the last CNOTs.
160+
yield _apply_cnots(previous_h, ())

cirq/ops/boolean_hamiltonian_test.py

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Copyright 2021 The Cirq Developers
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import itertools
15+
import math
16+
17+
import numpy as np
18+
import pytest
19+
import sympy.parsing.sympy_parser as sympy_parser
20+
21+
import cirq
22+
23+
24+
@pytest.mark.parametrize(
25+
'boolean_str',
26+
[
27+
'x0',
28+
'~x0',
29+
'x0 ^ x1',
30+
'x0 & x1',
31+
'x0 | x1',
32+
'x0 & x1 & x2',
33+
'x0 & x1 & ~x2',
34+
'x0 & ~x1 & x2',
35+
'x0 & ~x1 & ~x2',
36+
'~x0 & x1 & x2',
37+
'~x0 & x1 & ~x2',
38+
'~x0 & ~x1 & x2',
39+
'~x0 & ~x1 & ~x2',
40+
'x0 ^ x1 ^ x2',
41+
'x0 | (x1 & x2)',
42+
'x0 & (x1 | x2)',
43+
'(x0 ^ x1 ^ x2) | (x2 ^ x3 ^ x4)',
44+
'(x0 ^ x2 ^ x4) | (x1 ^ x2 ^ x3)',
45+
'x0 & x1 & (x2 | x3)',
46+
'x0 & ~x2',
47+
'~x0 & x2',
48+
'x2 & ~x0',
49+
'~x2 & x0',
50+
'(x2 | x1) ^ x0',
51+
],
52+
)
53+
def test_circuit(boolean_str):
54+
boolean_expr = sympy_parser.parse_expr(boolean_str)
55+
var_names = cirq.parameter_names(boolean_expr)
56+
qubits = [cirq.NamedQubit(name) for name in var_names]
57+
58+
# We use Sympy to evaluate the expression:
59+
n = len(var_names)
60+
61+
expected = []
62+
for binary_inputs in itertools.product([0, 1], repeat=n):
63+
subed_expr = boolean_expr
64+
for var_name, binary_input in zip(var_names, binary_inputs):
65+
subed_expr = subed_expr.subs(var_name, binary_input)
66+
expected.append(bool(subed_expr))
67+
68+
# We build a circuit and look at its output state vector:
69+
circuit = cirq.Circuit()
70+
circuit.append(cirq.H.on_each(*qubits))
71+
72+
hamiltonian_gate = cirq.BooleanHamiltonian(
73+
{q.name: q for q in qubits}, [boolean_str], 0.1 * math.pi
74+
)
75+
assert hamiltonian_gate.with_qubits(*qubits) == hamiltonian_gate
76+
77+
assert hamiltonian_gate.num_qubits() == n
78+
79+
circuit.append(hamiltonian_gate)
80+
81+
phi = cirq.Simulator().simulate(circuit, qubit_order=qubits, initial_state=0).state_vector()
82+
actual = np.arctan2(phi.real, phi.imag) - math.pi / 2.0 > 0.0
83+
84+
# Compare the two:
85+
np.testing.assert_array_equal(actual, expected)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[
2+
{
3+
"cirq_type": "BooleanHamiltonian",
4+
"qubit_map": {
5+
"q0": {
6+
"cirq_type": "NamedQubit",
7+
"name": "q0"
8+
}
9+
},
10+
"boolean_strs": [
11+
"q0"
12+
],
13+
"theta": 0.20160913
14+
}
15+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[cirq.BooleanHamiltonian({'q0': cirq.NamedQubit('q0')}, ['q0'], 0.20160913)]

0 commit comments

Comments
 (0)