Skip to content

Commit

Permalink
BooleanHamiltonianGate implementation (#4705)
Browse files Browse the repository at this point in the history
* BooleanHamiltonianGate implementation

* Replace dict with sequence for order preservation

* Remove logic for qudits, add check that only qubits are accepted.

* Deprecate boolham op
  • Loading branch information
daxfohl authored Feb 28, 2022
1 parent 8d65806 commit d2ae1e4
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 14 deletions.
1 change: 1 addition & 0 deletions cirq-core/cirq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@
bit_flip,
BitFlipChannel,
BooleanHamiltonian,
BooleanHamiltonianGate,
CCX,
CCXPowGate,
CCZ,
Expand Down
1 change: 1 addition & 0 deletions cirq-core/cirq/json_resolver_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def _parallel_gate_op(gate, qubits):
'BitFlipChannel': cirq.BitFlipChannel,
'BitstringAccumulator': cirq.work.BitstringAccumulator,
'BooleanHamiltonian': cirq.BooleanHamiltonian,
'BooleanHamiltonianGate': cirq.BooleanHamiltonianGate,
'CCNotPowGate': cirq.CCNotPowGate,
'CCXPowGate': cirq.CCXPowGate,
'CCZPowGate': cirq.CCZPowGate,
Expand Down
1 change: 1 addition & 0 deletions cirq-core/cirq/ops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

from cirq.ops.boolean_hamiltonian import (
BooleanHamiltonian,
BooleanHamiltonianGate,
)

from cirq.ops.common_channels import (
Expand Down
95 changes: 93 additions & 2 deletions cirq-core/cirq/ops/boolean_hamiltonian.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,20 @@
[4] Efficient Quantum Circuits for Diagonal Unitaries Without Ancillas by Jonathan Welch, Daniel
Greenbaum, Sarah Mostame, and Alán Aspuru-Guzik, https://arxiv.org/abs/1306.3991
"""
import itertools
import functools

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

import sympy.parsing.sympy_parser as sympy_parser

import cirq
from cirq import value
from cirq._compat import deprecated_class
from cirq.ops import raw_types
from cirq.ops.linear_combinations import PauliSum, PauliString


@deprecated_class(deadline='v0.15', fix='Use cirq.BooleanHamiltonianGate')
@value.value_equality
class BooleanHamiltonian(raw_types.Operation):
"""An operation that represents a Hamiltonian from a set of Boolean functions."""
Expand Down Expand Up @@ -64,7 +65,12 @@ def __init__(
boolean_strs: The list of Sympy-parsable Boolean expressions.
qubit_map: map of string (boolean variable name) to qubit.
theta: The evolution time (angle) for the Hamiltonian
Raises:
ValueError: If the any qubits are not 2D.
"""
if any(q.dimension != 2 for q in qubit_map.values()):
raise ValueError('All qubits must be 2-dimensional.')
self._qubit_map: Dict[str, 'cirq.Qid'] = qubit_map
self._boolean_strs: Sequence[str] = boolean_strs
self._theta: float = theta
Expand Down Expand Up @@ -114,6 +120,91 @@ def _decompose_(self):
hamiltonian_polynomial_list, self._qubit_map, self._theta
)

def _has_unitary_(self):
return True

@property
def gate(self) -> 'cirq.Gate':
return BooleanHamiltonianGate(
tuple(self._qubit_map.keys()), self._boolean_strs, self._theta
)


@value.value_equality
class BooleanHamiltonianGate(raw_types.Gate):
"""A gate that represents a Hamiltonian from a set of Boolean functions."""

def __init__(
self,
parameter_names: Sequence[str],
boolean_strs: Sequence[str],
theta: float,
):
"""Builds a BooleanHamiltonianGate.
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 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)
an XOR.
Then, we compute exp(-j * theta * polynomial), which is unitary because the polynomial is
Hermitian.
Args:
parameter_names: The names of the inputs to the expressions.
boolean_strs: The list of Sympy-parsable Boolean expressions.
theta: The evolution time (angle) for the Hamiltonian
"""
self._parameter_names: Sequence[str] = parameter_names
self._boolean_strs: Sequence[str] = boolean_strs
self._theta: float = theta

def _qid_shape_(self) -> Tuple[int, ...]:
return (2,) * len(self._parameter_names)

def _value_equality_values_(self) -> Any:
return self._parameter_names, self._boolean_strs, self._theta

def _json_dict_(self) -> Dict[str, Any]:
return {
'cirq_type': self.__class__.__name__,
'parameter_names': self._parameter_names,
'boolean_strs': self._boolean_strs,
'theta': self._theta,
}

@classmethod
def _from_json_dict_(
cls, parameter_names, boolean_strs, theta, **kwargs
) -> 'cirq.BooleanHamiltonianGate':
return cls(parameter_names, boolean_strs, theta)

def _decompose_(self, qubits: Sequence['cirq.Qid']) -> 'cirq.OP_TREE':
qubit_map = dict(zip(self._parameter_names, qubits))
boolean_exprs = [sympy_parser.parse_expr(boolean_str) for boolean_str in self._boolean_strs]
hamiltonian_polynomial_list = [
PauliSum.from_boolean_expression(boolean_expr, qubit_map)
for boolean_expr in boolean_exprs
]

return _get_gates_from_hamiltonians(hamiltonian_polynomial_list, qubit_map, self._theta)

def _has_unitary_(self) -> bool:
return True

def __repr__(self) -> str:
return (
f'cirq.BooleanHamiltonianGate('
f'parameter_names={self._parameter_names!r}, '
f'boolean_strs={self._boolean_strs!r}, '
f'theta={self._theta!r})'
)


def _gray_code_comparator(k1: Tuple[int, ...], k2: Tuple[int, ...], flip: bool = False) -> int:
"""Compares two Gray-encoded binary numbers.
Expand Down
61 changes: 49 additions & 12 deletions cirq-core/cirq/ops/boolean_hamiltonian_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
'(x2 | x1) ^ x0',
],
)
def test_circuit(boolean_str):
@pytest.mark.parametrize('transform', [lambda op: op, lambda op: op.gate.on(*op.qubits)])
def test_circuit(boolean_str, transform):
boolean_expr = sympy_parser.parse_expr(boolean_str)
var_names = cirq.parameter_names(boolean_expr)
qubits = [cirq.NamedQubit(name) for name in var_names]
Expand All @@ -72,13 +73,14 @@ def test_circuit(boolean_str):
circuit = cirq.Circuit()
circuit.append(cirq.H.on_each(*qubits))

hamiltonian_gate = cirq.BooleanHamiltonian(
{q.name: q for q in qubits}, [boolean_str], 0.1 * math.pi
)
with cirq.testing.assert_deprecated('Use cirq.BooleanHamiltonianGate', deadline='v0.15'):
hamiltonian_gate = cirq.BooleanHamiltonian(
{q.name: q for q in qubits}, [boolean_str], 0.1 * math.pi
)

assert hamiltonian_gate.num_qubits() == n

circuit.append(hamiltonian_gate)
circuit.append(transform(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
Expand All @@ -89,18 +91,53 @@ def test_circuit(boolean_str):

def test_with_custom_names():
q0, q1, q2, q3 = cirq.LineQubit.range(4)
original_op = cirq.BooleanHamiltonian(
{'a': q0, 'b': q1},
with cirq.testing.assert_deprecated(
'Use cirq.BooleanHamiltonianGate', deadline='v0.15', count=3
):
original_op = cirq.BooleanHamiltonian(
{'a': q0, 'b': q1},
['a'],
0.1,
)
assert cirq.decompose(original_op) == [cirq.Rz(rads=-0.05).on(q0)]

renamed_op = original_op.with_qubits(q2, q3)
assert cirq.decompose(renamed_op) == [cirq.Rz(rads=-0.05).on(q2)]

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

with pytest.raises(ValueError, match='All qubits must be 2-dimensional'):
original_op.with_qubits(q0, cirq.LineQid(1, 3))


def test_gate_with_custom_names():
q0, q1, q2, q3 = cirq.LineQubit.range(4)
gate = cirq.BooleanHamiltonianGate(
['a', 'b'],
['a'],
0.1,
)
assert cirq.decompose(original_op) == [cirq.Rz(rads=-0.05).on(q0)]
assert cirq.decompose(gate.on(q0, q1)) == [cirq.Rz(rads=-0.05).on(q0)]
assert cirq.decompose_once_with_qubits(gate, (q0, q1)) == [cirq.Rz(rads=-0.05).on(q0)]
assert cirq.decompose(gate.on(q2, q3)) == [cirq.Rz(rads=-0.05).on(q2)]
assert cirq.decompose_once_with_qubits(gate, (q2, q3)) == [cirq.Rz(rads=-0.05).on(q2)]

with pytest.raises(ValueError, match='Wrong number of qubits'):
gate.on(q2)
with pytest.raises(ValueError, match='Wrong shape of qids'):
gate.on(q0, cirq.LineQid(1, 3))

renamed_op = original_op.with_qubits(q2, q3)
assert cirq.decompose(renamed_op) == [cirq.Rz(rads=-0.05).on(q2)]

with pytest.raises(ValueError, match='Length of replacement qubits must be the same'):
original_op.with_qubits(q2)
def test_gate_consistent():
gate = cirq.BooleanHamiltonianGate(
['a', 'b'],
['a'],
0.1,
)
op = gate.on(*cirq.LineQubit.range(2))
cirq.testing.assert_implements_consistent_protocols(gate)
cirq.testing.assert_implements_consistent_protocols(op)


@pytest.mark.parametrize(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"cirq_type": "BooleanHamiltonianGate",
"parameter_names": ["q0", "q1"],
"boolean_strs": ["q0"],
"theta": 0.20160913
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[cirq.BooleanHamiltonianGate(parameter_names=['q0', 'q1'], boolean_strs=['q0'], theta=0.20160913)]
1 change: 1 addition & 0 deletions cirq-core/cirq/protocols/json_test_data/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
],
deprecated={
'GlobalPhaseOperation': 'v0.16',
'BooleanHamiltonian': 'v0.15',
'SymmetricalQidPair': 'v0.15',
},
tested_elsewhere=[
Expand Down

0 comments on commit d2ae1e4

Please sign in to comment.