diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index 59264fe3bb4..ae2bf4f457f 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -196,6 +196,7 @@ bit_flip, BitFlipChannel, BooleanHamiltonian, + BooleanHamiltonianGate, CCX, CCXPowGate, CCZ, diff --git a/cirq-core/cirq/json_resolver_cache.py b/cirq-core/cirq/json_resolver_cache.py index 44ece45bc90..f15252c3256 100644 --- a/cirq-core/cirq/json_resolver_cache.py +++ b/cirq-core/cirq/json_resolver_cache.py @@ -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, diff --git a/cirq-core/cirq/ops/__init__.py b/cirq-core/cirq/ops/__init__.py index 552b5a7119e..5ff9c8f4baa 100644 --- a/cirq-core/cirq/ops/__init__.py +++ b/cirq-core/cirq/ops/__init__.py @@ -32,6 +32,7 @@ from cirq.ops.boolean_hamiltonian import ( BooleanHamiltonian, + BooleanHamiltonianGate, ) from cirq.ops.common_channels import ( diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py index 2e92e577e75..f4fbf019d6c 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian.py @@ -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.""" @@ -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 @@ -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. diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py index a7c5c65eee7..4932450836d 100644 --- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py +++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py @@ -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] @@ -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 @@ -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( diff --git a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianGate.json b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianGate.json new file mode 100644 index 00000000000..23d6e7a221b --- /dev/null +++ b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianGate.json @@ -0,0 +1,8 @@ +[ + { + "cirq_type": "BooleanHamiltonianGate", + "parameter_names": ["q0", "q1"], + "boolean_strs": ["q0"], + "theta": 0.20160913 + } +] \ No newline at end of file diff --git a/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianGate.repr b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianGate.repr new file mode 100644 index 00000000000..0d1c4055ad9 --- /dev/null +++ b/cirq-core/cirq/protocols/json_test_data/BooleanHamiltonianGate.repr @@ -0,0 +1 @@ +[cirq.BooleanHamiltonianGate(parameter_names=['q0', 'q1'], boolean_strs=['q0'], theta=0.20160913)] \ No newline at end of file diff --git a/cirq-core/cirq/protocols/json_test_data/spec.py b/cirq-core/cirq/protocols/json_test_data/spec.py index 98f3e7b13da..f23a193d5ac 100644 --- a/cirq-core/cirq/protocols/json_test_data/spec.py +++ b/cirq-core/cirq/protocols/json_test_data/spec.py @@ -190,6 +190,7 @@ ], deprecated={ 'GlobalPhaseOperation': 'v0.16', + 'BooleanHamiltonian': 'v0.15', 'SymmetricalQidPair': 'v0.15', }, tested_elsewhere=[