diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index db01050dd23..0dd88425a63 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -383,6 +383,9 @@

Other improvements

+* `qml.quantum_monte_carlo` now uses the new transform system. + [(#4708)](https://github.com/PennyLaneAI/pennylane/pull/4708/) + * `qml.simplify` now uses the new transforms API. [(#4949)](https://github.com/PennyLaneAI/pennylane/pull/4949) @@ -648,10 +651,9 @@ Juan Giraldo, Emiliano Godinez Ramirez, Ankit Khandelwal, Christina Lee, -Romain Moyard, Vincent Michaud-Rioux, -Romain Moyard, Anurav Modak, +Romain Moyard, Mudit Pandey, Matthew Silverman, Jay Soni, diff --git a/pennylane/transforms/qmc.py b/pennylane/transforms/qmc.py index c86a0170cda..995f894759e 100644 --- a/pennylane/transforms/qmc.py +++ b/pennylane/transforms/qmc.py @@ -14,10 +14,14 @@ """ Contains the quantum_monte_carlo transform. """ -from functools import wraps +from copy import copy +from typing import Sequence, Callable + +import pennylane as qml from pennylane import PauliX, Hadamard, MultiControlledX, CZ, adjoint from pennylane.wires import Wires from pennylane.templates import QFT +from pennylane.transforms.core import transform def _apply_controlled_z(wires, control_wire, work_wires): @@ -27,7 +31,7 @@ def _apply_controlled_z(wires, control_wire, work_wires): The multi-qubit gate :math:`Z = I - 2|0\rangle \langle 0|` can be performed using the conventional multi-controlled-Z gate with an additional bit flip on each qubit before and after. - This function performs the multi-controlled-Z gate via a multi-controlled-X gate by picking an + This function returns the multi-controlled-Z gate via a multi-controlled-X gate by picking an arbitrary target wire to perform the X and adding a Hadamard on that wire either side of the transformation. @@ -39,19 +43,24 @@ def _apply_controlled_z(wires, control_wire, work_wires): work_wires (Wires): the work wires used in the decomposition """ target_wire = wires[0] - PauliX(target_wire) - Hadamard(target_wire) + updated_operations = [] + updated_operations.append(PauliX(target_wire)) + updated_operations.append(Hadamard(target_wire)) control_values = "0" * (len(wires) - 1) + "1" control_wires = wires[1:] + control_wire - MultiControlledX( - wires=[*control_wires, target_wire], - control_values=control_values, - work_wires=work_wires, + updated_operations.append( + MultiControlledX( + wires=[*control_wires, target_wire], + control_values=control_values, + work_wires=work_wires, + ) ) - Hadamard(target_wire) - PauliX(target_wire) + updated_operations.append(Hadamard(target_wire)) + updated_operations.append(PauliX(target_wire)) + + return updated_operations def _apply_controlled_v(target_wire, control_wire): @@ -67,14 +76,17 @@ def _apply_controlled_v(target_wire, control_wire): target_wire (Wires): the ancilla wire in which the expectation value is encoded control_wire (Wires): the control wire from the register of phase estimation qubits """ - CZ(wires=[control_wire[0], target_wire[0]]) + return [CZ(wires=[control_wire[0], target_wire[0]])] -def apply_controlled_Q(fn, wires, target_wire, control_wire, work_wires): - r"""Provides the circuit to apply a controlled version of the :math:`\mathcal{Q}` unitary +@transform +def apply_controlled_Q( + tape: qml.tape.QuantumTape, wires, target_wire, control_wire, work_wires +) -> (Sequence[qml.tape.QuantumTape], Callable): + r"""Applies the transform that performs a controlled version of the :math:`\mathcal{Q}` unitary defined in `this `__ paper. - The input ``fn`` should be the quantum circuit corresponding to the :math:`\mathcal{F}` unitary + The input ``tape`` should be the quantum circuit corresponding to the :math:`\mathcal{F}` unitary in the paper above. This function transforms this circuit into a controlled version of the :math:`\mathcal{Q}` unitary, which forms part of the quantum Monte Carlo algorithm. The :math:`\mathcal{Q}` unitary encodes the target expectation value as a phase in one of its @@ -82,8 +94,8 @@ def apply_controlled_Q(fn, wires, target_wire, control_wire, work_wires): :class:`~.QuantumPhaseEstimation` for more details). Args: - fn (Callable): a quantum function that applies quantum operations according to the - :math:`\mathcal{F}` unitary used as part of quantum Monte Carlo estimation + tape (QNode or QuantumTape or Callable): the quantum circuit that applies quantum operations + according to the :math:`\mathcal{F}` unitary used as part of quantum Monte Carlo estimation wires (Union[Wires or Sequence[int]]): the wires acted upon by the ``fn`` circuit target_wire (Union[Wires, int]): The wire in which the expectation value is encoded. Must be contained within ``wires``. @@ -93,41 +105,57 @@ def apply_controlled_Q(fn, wires, target_wire, control_wire, work_wires): decomposing :math:`\mathcal{Q}` Returns: - function: The input function transformed to the :math:`\mathcal{Q}` unitary + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will perform control on :math:`\mathcal{Q}` unitary. Raises: ValueError: if ``target_wire`` is not in ``wires`` """ - fn_inv = adjoint(fn) - - wires = Wires(wires) - target_wire = Wires(target_wire) - control_wire = Wires(control_wire) - work_wires = Wires(work_wires) - - if not wires.contains_wires(target_wire): - raise ValueError("The target wire must be contained within wires") - - @wraps(fn) - def wrapper(*args, **kwargs): - _apply_controlled_v(target_wire=target_wire, control_wire=control_wire) - fn_inv(*args, **kwargs) - _apply_controlled_z(wires=wires, control_wire=control_wire, work_wires=work_wires) - fn(*args, **kwargs) - - _apply_controlled_v(target_wire=target_wire, control_wire=control_wire) - fn_inv(*args, **kwargs) - _apply_controlled_z(wires=wires, control_wire=control_wire, work_wires=work_wires) - fn(*args, **kwargs) - - return wrapper - - -def quantum_monte_carlo(fn, wires, target_wire, estimation_wires): - r"""Provides the circuit to perform the + operations = tape.operations.copy() + updated_operations = [] + + with qml.queuing.QueuingManager.stop_recording(): + op_inv = [adjoint(copy(op)) for op in reversed(operations)] + + wires = Wires(wires) + target_wire = Wires(target_wire) + control_wire = Wires(control_wire) + work_wires = Wires(work_wires) + + if not wires.contains_wires(target_wire): + raise ValueError("The target wire must be contained within wires") + + updated_operations.extend( + _apply_controlled_v(target_wire=target_wire, control_wire=control_wire) + ) + updated_operations.extend(op_inv) + updated_operations.extend( + _apply_controlled_z(wires=wires, control_wire=control_wire, work_wires=work_wires) + ) + updated_operations.extend(operations) + updated_operations.extend( + _apply_controlled_v(target_wire=target_wire, control_wire=control_wire) + ) + updated_operations.extend(op_inv) + updated_operations.extend( + _apply_controlled_z(wires=wires, control_wire=control_wire, work_wires=work_wires) + ) + updated_operations.extend(operations) + + tape = type(tape)(updated_operations, tape.measurements, shots=tape.shots) + return [tape], lambda x: x[0] + + +@transform +def quantum_monte_carlo( + tape: qml.tape.QuantumTape, wires, target_wire, estimation_wires +) -> (Sequence[qml.tape.QuantumTape], Callable): + r"""Applies the transform `quantum Monte Carlo estimation `__ algorithm. - The input ``fn`` should be the quantum circuit corresponding to the :math:`\mathcal{F}` unitary + The input `tape`` should be the quantum circuit corresponding to the :math:`\mathcal{F}` unitary in the paper above. This unitary encodes the probability distribution and random variable onto ``wires`` so that measurement of the ``target_wire`` provides the expectation value to be estimated. The quantum Monte Carlo algorithm then estimates the expectation value using quantum @@ -146,7 +174,7 @@ def quantum_monte_carlo(fn, wires, target_wire, estimation_wires): simulators, but may perform faster and is suited to quick prototyping. Args: - fn (Callable): a quantum function that applies quantum operations according to the + tape (QNode or QuantumTape or Callable): the quantum circuit that applies quantum operations according to the :math:`\mathcal{F}` unitary used as part of quantum Monte Carlo estimation wires (Union[Wires or Sequence[int]]): the wires acted upon by the ``fn`` circuit target_wire (Union[Wires, int]): The wire in which the expectation value is encoded. Must be @@ -154,7 +182,11 @@ def quantum_monte_carlo(fn, wires, target_wire, estimation_wires): estimation_wires (Union[Wires, Sequence[int], or int]): the wires used for phase estimation Returns: - function: The circuit for quantum Monte Carlo estimation + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will perform the quantum Monte Carlo estimation. + Raises: ValueError: if ``wires`` and ``estimation_wires`` share a common wire @@ -322,6 +354,7 @@ def qmc(): 'diff_method': 'best', 'gradient_fn': 'backprop'} """ + operations = tape.operations.copy() wires = Wires(wires) target_wire = Wires(target_wire) estimation_wires = Wires(estimation_wires) @@ -329,27 +362,27 @@ def qmc(): if Wires.shared_wires([wires, estimation_wires]): raise ValueError("No wires can be shared between the wires and estimation_wires registers") - @wraps(fn) - def wrapper(*args, **kwargs): - fn(*args, **kwargs) + updated_operations = [] + with qml.queuing.QueuingManager.stop_recording(): + updated_operations.extend(operations) for i, control_wire in enumerate(estimation_wires): - Hadamard(control_wire) + updated_operations.append(Hadamard(control_wire)) # Find wires eligible to be used as helper wires work_wires = estimation_wires.toset() - {control_wire} n_reps = 2 ** (len(estimation_wires) - (i + 1)) - q = apply_controlled_Q( - fn, + tapes_q, _ = apply_controlled_Q( + tape, wires=wires, target_wire=target_wire, control_wire=control_wire, work_wires=work_wires, ) - + tape_q = tapes_q[0] for _ in range(n_reps): - q(*args, **kwargs) - - adjoint(QFT(wires=estimation_wires)) + updated_operations.extend(tape_q.operations) - return wrapper + updated_operations.append(adjoint(QFT(wires=estimation_wires), lazy=False)) + updated_tape = type(tape)(updated_operations, tape.measurements, shots=tape.shots) + return [updated_tape], lambda x: x[0] diff --git a/tests/transforms/test_qmc_transform.py b/tests/transforms/test_qmc_transform.py index 61446d0c6aa..18c0e5ffc2d 100644 --- a/tests/transforms/test_qmc_transform.py +++ b/tests/transforms/test_qmc_transform.py @@ -176,7 +176,7 @@ def test_raises(self): with pytest.raises(ValueError, match="The target wire must be contained within wires"): apply_controlled_Q( lambda: ..., wires=range(3), target_wire=4, control_wire=5, work_wires=None - ) + )() class TestQuantumMonteCarlo: @@ -225,7 +225,7 @@ def test_shared_wires(self): with pytest.raises(ValueError, match="No wires can be shared between the wires"): quantum_monte_carlo( lambda: None, wires=wires, target_wire=0, estimation_wires=estimation_wires - ) + )() @pytest.mark.slow def test_integration(self): @@ -258,27 +258,21 @@ def fn(): fn, wires=wires, target_wire=target_wire, estimation_wires=estimation_wires ) - with qml.queuing.AnnotatedQueue() as q: - qmc_circuit() - qml.probs(estimation_wires) - - tape = qml.tape.QuantumScript.from_queue(q) - tape = tape.expand(depth=2) - - assert all( - not isinstance(op, (qml.MultiControlledX, qml.templates.QFT, qml.tape.QuantumScript)) - for op in tape.operations - ) - dev = qml.device("default.qubit", wires=wires + estimation_wires) - res = dev.execute(tape) @qml.qnode(dev) def circuit(): + qmc_circuit() + return qml.probs(estimation_wires) + + @qml.qnode(dev) + def circuit_expected(): qml.templates.QuantumMonteCarlo( probs, func, target_wires=wires, estimation_wires=estimation_wires ) return qml.probs(estimation_wires) - res_expected = circuit() + res = circuit() + res_expected = circuit_expected() + assert np.allclose(res, res_expected)