Skip to content

Commit e340d0b

Browse files
authored
Add an option to build a PauliSum from a Sympy Boolean expression (quantumlib#4282)
As part of the the review quantumlib#3989 we could use the ability to build a PauliSum from a Boolean expression. This PR makes available that function (smaller change).
1 parent b0c70d6 commit e340d0b

File tree

2 files changed

+91
-0
lines changed

2 files changed

+91
-0
lines changed

cirq/ops/linear_combinations.py

+54
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from collections import defaultdict
1515
from typing import (
1616
AbstractSet,
17+
Dict,
1718
Iterable,
1819
Mapping,
1920
Optional,
@@ -27,6 +28,9 @@
2728
import numbers
2829

2930
import numpy as np
31+
from sympy.logic.boolalg import And, Not, Or, Xor
32+
from sympy.core.expr import Expr
33+
from sympy.core.symbol import Symbol
3034

3135
from cirq import linalg, protocols, qis, value
3236
from cirq._doc import document
@@ -398,6 +402,56 @@ def from_pauli_strings(cls, terms: Union[PauliString, List[PauliString]]) -> 'Pa
398402
termdict[key] += pstring.coefficient
399403
return cls(linear_dict=value.LinearDict(termdict))
400404

405+
@classmethod
406+
def from_boolean_expression(
407+
cls, boolean_expr: Expr, qubit_map: Dict[str, 'cirq.Qid']
408+
) -> 'PauliSum':
409+
"""Builds the Hamiltonian representation of a Boolean expression.
410+
411+
This is based on "On the representation of Boolean and real functions as Hamiltonians for
412+
quantum computing" by Stuart Hadfield, https://arxiv.org/abs/1804.09130
413+
414+
Args:
415+
boolean_expr: A Sympy expression containing symbols and Boolean operations
416+
qubit_map: map of string (boolean variable name) to qubit.
417+
418+
Return:
419+
The PauliString that represents the Boolean expression.
420+
"""
421+
if isinstance(boolean_expr, Symbol):
422+
# In table 1, the entry for 'x' is '1/2.I - 1/2.Z'
423+
return cls.from_pauli_strings(
424+
[
425+
PauliString({}, 0.5),
426+
PauliString({qubit_map[boolean_expr.name]: pauli_gates.Z}, -0.5),
427+
]
428+
)
429+
430+
if isinstance(boolean_expr, (And, Not, Or, Xor)):
431+
sub_pauli_sums = [
432+
cls.from_boolean_expression(sub_boolean_expr, qubit_map)
433+
for sub_boolean_expr in boolean_expr.args
434+
]
435+
# We apply the equalities of theorem 1.
436+
if isinstance(boolean_expr, And):
437+
pauli_sum = cls.from_pauli_strings(PauliString({}, 1.0))
438+
for sub_pauli_sum in sub_pauli_sums:
439+
pauli_sum = pauli_sum * sub_pauli_sum
440+
elif isinstance(boolean_expr, Not):
441+
assert len(sub_pauli_sums) == 1
442+
pauli_sum = cls.from_pauli_strings(PauliString({}, 1.0)) - sub_pauli_sums[0]
443+
elif isinstance(boolean_expr, Or):
444+
pauli_sum = cls.from_pauli_strings(PauliString({}, 0.0))
445+
for sub_pauli_sum in sub_pauli_sums:
446+
pauli_sum = pauli_sum + sub_pauli_sum - pauli_sum * sub_pauli_sum
447+
elif isinstance(boolean_expr, Xor):
448+
pauli_sum = cls.from_pauli_strings(PauliString({}, 0.0))
449+
for sub_pauli_sum in sub_pauli_sums:
450+
pauli_sum = pauli_sum + sub_pauli_sum - 2.0 * pauli_sum * sub_pauli_sum
451+
return pauli_sum
452+
453+
raise ValueError(f'Unsupported type: {type(boolean_expr)}')
454+
401455
@property
402456
def qubits(self) -> Tuple[raw_types.Qid, ...]:
403457
qs = {q for k in self._linear_dict.keys() for q, _ in k}

cirq/ops/linear_combinations_test.py

+37
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import numpy as np
1919
import pytest
2020
import sympy
21+
import sympy.parsing.sympy_parser as sympy_parser
2122

2223
import cirq
2324
import cirq.testing
@@ -1352,6 +1353,42 @@ def test_pauli_sum_pow():
13521353
assert cirq.approx_eq(psum ** 0, identity)
13531354

13541355

1356+
# Using the entries of table 1 of https://arxiv.org/abs/1804.09130 as golden values.
1357+
@pytest.mark.parametrize(
1358+
'boolean_expr,expected_pauli_sum',
1359+
[
1360+
('x', ['(-0.5+0j)*Z(x)', '(0.5+0j)*I']),
1361+
('~x', ['(0.5+0j)*I', '(0.5+0j)*Z(x)']),
1362+
('x0 ^ x1', ['(-0.5+0j)*Z(x0)*Z(x1)', '(0.5+0j)*I']),
1363+
(
1364+
'x0 & x1',
1365+
['(-0.25+0j)*Z(x0)', '(-0.25+0j)*Z(x1)', '(0.25+0j)*I', '(0.25+0j)*Z(x0)*Z(x1)'],
1366+
),
1367+
(
1368+
'x0 | x1',
1369+
['(-0.25+0j)*Z(x0)', '(-0.25+0j)*Z(x0)*Z(x1)', '(-0.25+0j)*Z(x1)', '(0.75+0j)*I'],
1370+
),
1371+
('x0 ^ x1 ^ x2', ['(-0.5+0j)*Z(x0)*Z(x1)*Z(x2)', '(0.5+0j)*I']),
1372+
],
1373+
)
1374+
def test_from_boolean_expression(boolean_expr, expected_pauli_sum):
1375+
boolean = sympy_parser.parse_expr(boolean_expr)
1376+
qubit_map = {name: cirq.NamedQubit(name) for name in sorted(cirq.parameter_names(boolean))}
1377+
actual = cirq.PauliSum.from_boolean_expression(boolean, qubit_map)
1378+
# Instead of calling str() directly, first make sure that the items are sorted. This is to make
1379+
# the unit test more robut in case Sympy would result in a different parsing order. By sorting
1380+
# the individual items, we would have a canonical representation.
1381+
actual_items = list(sorted(str(pauli_string) for pauli_string in actual))
1382+
assert expected_pauli_sum == actual_items
1383+
1384+
1385+
def test_unsupported_op():
1386+
not_a_boolean = sympy_parser.parse_expr('x * x')
1387+
qubit_map = {name: cirq.NamedQubit(name) for name in cirq.parameter_names(not_a_boolean)}
1388+
with pytest.raises(ValueError, match='Unsupported type'):
1389+
cirq.PauliSum.from_boolean_expression(not_a_boolean, qubit_map)
1390+
1391+
13551392
def test_imul_aliasing():
13561393
q0, q1, q2 = cirq.LineQubit.range(3)
13571394
psum1 = cirq.X(q0) + cirq.Y(q1)

0 commit comments

Comments
 (0)