Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BooleanHamiltonianGate implementation #4705

Merged
merged 15 commits into from
Feb 28, 2022
1 change: 1 addition & 0 deletions cirq-core/cirq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,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 @@ -56,6 +56,7 @@ def _parallel_gate_op(gate, qubits):
'BitFlipChannel': cirq.BitFlipChannel,
'BitstringAccumulator': cirq.work.BitstringAccumulator,
'BooleanHamiltonian': cirq.BooleanHamiltonian,
'BooleanHamiltonianGate': cirq.BooleanHamiltonianGate,
'ProductState': cirq.ProductState,
'CCNotPowGate': cirq.CCNotPowGate,
'CCXPowGate': cirq.CCXPowGate,
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 @@ -31,6 +31,7 @@

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

from cirq.ops.common_channels import (
Expand Down
95 changes: 94 additions & 1 deletion cirq-core/cirq/ops/boolean_hamiltonian.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

import cirq
from cirq import value
from cirq.ops import raw_types
from cirq.ops import gate_operation, raw_types
from cirq.ops.linear_combinations import PauliSum, PauliString


Expand Down Expand Up @@ -64,7 +64,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 @@ -115,6 +120,94 @@ def _decompose_(self):
hamiltonian_polynomial_list, self._qubit_map, self._theta
)

def _has_unitary_(self):
return True

def __repr__(self):
return (
f'cirq.BooleanHamiltonian('
f'qubit_map={self._qubit_map!r}, '
f'boolean_strs={self._boolean_strs!r}, '
f'theta={self._theta!r})'
)


@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):
return (2,) * len(self._parameter_names)

def on(self, *qubits) -> 'cirq.Operation':
return gate_operation.GateOperation(self, qubits)

def _value_equality_values_(self):
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):
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):
return True

def __repr__(self):
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
105 changes: 105 additions & 0 deletions cirq-core/cirq/ops/boolean_hamiltonian_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,69 @@ def test_circuit(boolean_str):
np.testing.assert_array_equal(actual, expected)


@pytest.mark.parametrize(
'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_gate_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]

# We use Sympy to evaluate the expression:
n = len(var_names)

expected = []
for binary_inputs in itertools.product([0, 1], repeat=n):
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 = cirq.Circuit()
circuit.append(cirq.H.on_each(*qubits))

hamiltonian_gate = cirq.BooleanHamiltonianGate(
[q.name for q in qubits], [boolean_str], 0.1 * math.pi
).on(*qubits)

assert cirq.qid_shape(hamiltonian_gate) == cirq.qid_shape(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

# Compare the two:
np.testing.assert_array_equal(actual, expected)


def test_with_custom_names():
q0, q1, q2, q3 = cirq.LineQubit.range(4)
original_op = cirq.BooleanHamiltonian(
Expand All @@ -102,6 +165,48 @@ def test_with_custom_names():
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(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))


def test_consistent():
q0, q1 = cirq.LineQubit.range(2)
op = cirq.BooleanHamiltonian(
{'a': q0, 'b': q1},
['a'],
0.1,
)
cirq.testing.assert_implements_consistent_protocols(op)


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(
'n_bits,expected_hs',
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)]