diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 8270689873e..28a2c45fe2e 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -104,13 +104,108 @@ - The dependency on openbabel is removed [(#2415)](https://github.com/PennyLaneAI/pennylane/pull/2415) -* Development of a circuit-cutting compiler extension to circuits with sampling - measurements has begun: - - - The existing `qcut.tape_to_graph()` method has been extended to convert a - sample measurement without an observable specified to multiple single-qubit sample - nodes. +*

Finite-shot circuit cutting ✂️

+ + * You can now run `N`-wire circuits containing sample-based measurements on + devices with fewer than `N` wires by inserting `WireCut` operations into + the circuit and decorating your QNode with `@qml.cut_circuit_mc`. + With this, samples from the original circuit can be simulated using + a Monte Carlo method, + using fewer qubits at the expense of more device executions. Additionally, + this transform + can take an optional classical processing function as an argument + and return an expectation value. [(#2313)](https://github.com/PennyLaneAI/pennylane/pull/2313) + [(#2321)](https://github.com/PennyLaneAI/pennylane/pull/2321) + [(#2332)](https://github.com/PennyLaneAI/pennylane/pull/2332) + [(#2358)](https://github.com/PennyLaneAI/pennylane/pull/2358) + [(#2382)](https://github.com/PennyLaneAI/pennylane/pull/2382) + [(#2399)](https://github.com/PennyLaneAI/pennylane/pull/2399) + [(#2407)](https://github.com/PennyLaneAI/pennylane/pull/2407) + + The following `3`-qubit circuit contains a `WireCut` operation and a `sample` + measurement. When decorated with `@qml.cut_circuit_mc`, we can cut the circuit + into two `2`-qubit fragments: + + ```python + + dev = qml.device("default.qubit", wires=2, shots=1000) + + @qml.cut_circuit_mc + @qml.qnode(dev) + def circuit(x): + qml.RX(0.89, wires=0) + qml.RY(0.5, wires=1) + qml.RX(1.3, wires=2) + + qml.CNOT(wires=[0, 1]) + qml.WireCut(wires=1) + qml.CNOT(wires=[1, 2]) + + qml.RX(x, wires=0) + qml.RY(0.7, wires=1) + qml.RX(2.3, wires=2) + return qml.sample(wires=[0, 2]) + ``` + + we can then execute the circuit as usual by calling the QNode: + + ```pycon + >>> x = 0.3 + >>> circuit(x) + tensor([[1, 1], + [0, 1], + [0, 1], + ..., + [0, 1], + [0, 1], + [0, 1]], requires_grad=True) + ``` + + Furthermore, the number of shots can be temporarily altered when calling + the QNode: + + ```pycon + >>> results = circuit(x, shots=123) + >>> results.shape + (123, 2) + ``` + + Using the Monte Carlo approach of [Peng et. al](https://arxiv.org/abs/1904.00102), the + `cut_circuit_mc` transform also supports returning sample-based expectation values of + observables that are diagonal in the computational basis, as shown below for a `ZZ` measurement + on wires `0` and `2`: + + ```python + dev = qml.device("default.qubit", wires=2, shots=10000) + + def observable(bitstring): + return (-1) ** np.sum(bitstring) + + @qml.cut_circuit_mc(classical_processing_fn=observable) + @qml.qnode(dev) + def circuit(x): + qml.RX(0.89, wires=0) + qml.RY(0.5, wires=1) + qml.RX(1.3, wires=2) + + qml.CNOT(wires=[0, 1]) + qml.WireCut(wires=1) + qml.CNOT(wires=[1, 2]) + + qml.RX(x, wires=0) + qml.RY(0.7, wires=1) + qml.RX(2.3, wires=2) + return qml.sample(wires=[0, 2]) + ``` + + We can now approximate the expectation value of the observable using + + ```pycon + >>> circuit(x) + tensor(-0.776, requires_grad=True) + ``` + - An automatic graph partitioning method `qcut.kahypar_cut()` has been implemented for cutting arbitrary tape-converted graphs using the general purpose graph partitioning framework [KaHyPar](https://pypi.org/project/kahypar/) which needs to be installed separately. @@ -118,30 +213,6 @@ utilities are implemented which uses `qcut.kahypar_cut()` as the default auto cutter. [(#2330)](https://github.com/PennyLaneAI/pennylane/pull/2330) - - The existing `qcut.graph_to_tape()` method has been extended to convert - graphs containing sample measurement nodes to tapes. - [(#2321)](https://github.com/PennyLaneAI/pennylane/pull/2321) - - - A `qcut.expand_fragment_tapes_mc()` method has been added to expand fragment - tapes to random configurations by replacing measure and prepare nodes with - sampled Pauli measurements and state preparations. - [(#2332)](https://github.com/PennyLaneAI/pennylane/pull/2332) - - - Postprocessing functions `qcut.qcut_processing_fn_sample()` and - `qcut.qcut_processing_fn_mc()` have been added to return samples and expectation - values, respectively, of recombined fragments using the Monte Carlo sampling - approach. - [(#2358)](https://github.com/PennyLaneAI/pennylane/pull/2358) - - - A user-facing transform for circuit cutting with sample measurements has - been added. A `qnode` containing `WireCut` operations and `sample` measurements - can be decorated with `@qml.cut_circuit_mc()` to perform this type of cutting. - [(#2382)](https://github.com/PennyLaneAI/pennylane/pull/2382) - - - Add expansion to `qcut.cut_circuit_mc()` to search for wire cuts in - contained operations or tapes. - [(#2399)](https://github.com/PennyLaneAI/pennylane/pull/2399) -

Improvements

* The parameter-shift Hessian can now be computed for arbitrary diff --git a/pennylane/transforms/qcut.py b/pennylane/transforms/qcut.py index d2cf867ff1e..ba24b6637e0 100644 --- a/pennylane/transforms/qcut.py +++ b/pennylane/transforms/qcut.py @@ -946,10 +946,10 @@ def qcut_processing_fn_mc( @batch_transform def cut_circuit_mc( tape: QuantumTape, - shots: Optional[int] = None, - device_wires: Optional[Wires] = None, classical_processing_fn: Optional[callable] = None, max_depth: int = 1, + shots: Optional[int] = None, + device_wires: Optional[Wires] = None, ) -> Tuple[Tuple[QuantumTape], Callable]: """ Cut up a circuit containing sample measurements into smaller fragments using a @@ -1930,10 +1930,10 @@ def _cut_circuit_expand( def _cut_circuit_mc_expand( tape: QuantumTape, - shots: Optional[int] = None, - device_wires: Optional[Wires] = None, classical_processing_fn: Optional[callable] = None, max_depth: int = 1, + shots: Optional[int] = None, + device_wires: Optional[Wires] = None, ): """Main entry point for expanding operations in sample-based tapes until reaching a depth that includes :class:`~.WireCut` operations.""" diff --git a/tests/transforms/test_qcut.py b/tests/transforms/test_qcut.py index 49a70e4b5a9..df47c372764 100644 --- a/tests/transforms/test_qcut.py +++ b/tests/transforms/test_qcut.py @@ -82,6 +82,20 @@ def kron(*args): return np.kron(args[0], kron(*args[1:])) +def fn(x): + """ + Classical processing function for MC circuit cutting + """ + if x[0] == 0 and x[1] == 0: + return 1 + if x[0] == 0 and x[1] == 1: + return -1 + if x[0] == 1 and x[1] == 0: + return -1 + if x[0] == 1 and x[1] == 1: + return 1 + + # tape containing mid-circuit measurements with qml.tape.QuantumTape() as mcm_tape: qml.Hadamard(wires=0) @@ -2020,16 +2034,6 @@ def test_mc_sample_postprocess(self, interface, mocker): ] convert_fixed_samples = [qml.math.convert_like(fs, lib.ones(1)) for fs in fixed_samples] - def fn(x): - if x[0] == 0 and x[1] == 0: - return 1 - if x[0] == 0 and x[1] == 1: - return -1 - if x[0] == 1 and x[1] == 0: - return -1 - if x[0] == 1 and x[1] == 1: - return 1 - fixed_settings = np.array([[0, 7, 1], [5, 7, 2], [1, 0, 3], [5, 1, 1]]) spy_prod = mocker.spy(np, "prod") @@ -2165,19 +2169,9 @@ def target_circuit(v): qml.RX(2.3, wires=2) return qml.expval(qml.PauliZ(wires=0) @ qml.PauliZ(wires=2)) - def fn(x): - if x[0] == 0 and x[1] == 0: - return 1 - if x[0] == 0 and x[1] == 1: - return -1 - if x[0] == 1 and x[1] == 0: - return -1 - if x[0] == 1 and x[1] == 1: - return 1 - dev = qml.device("default.qubit", wires=2, shots=10000) - @qml.cut_circuit_mc(classical_processing_fn=fn) + @qml.cut_circuit_mc(fn) @qml.qnode(dev) def circuit(v): qml.RX(v, wires=0) @@ -2468,6 +2462,221 @@ def cut_circuit(x): assert res.shape == (shots, 2) assert type(res) == np.ndarray + @pytest.mark.parametrize( + "interface_import,interface", + [ + ("autograd.numpy", "autograd"), + ("tensorflow", "tensorflow"), + ("torch", "torch"), + ("jax.numpy", "jax"), + ], + ) + def test_all_interfaces_samples(self, interface_import, interface): + """ + Tests that `cut_circuit_mc` returns the correct type of sample + output value in all interfaces + """ + lib = pytest.importorskip(interface_import) + + shots = 10 + dev = qml.device("default.qubit", wires=2, shots=shots) + + @qml.cut_circuit_mc + @qml.qnode(dev, interface=interface) + def cut_circuit(x): + qml.RX(x, wires=0) + qml.RY(0.5, wires=1) + qml.RX(1.3, wires=2) + + qml.CNOT(wires=[0, 1]) + qml.WireCut(wires=1) + qml.CNOT(wires=[1, 2]) + + qml.RX(x, wires=0) + qml.RY(0.7, wires=1) + qml.RX(2.3, wires=2) + return qml.sample(wires=[0, 2]) + + v = 0.319 + convert_input = qml.math.convert_like(v, lib.ones(1)) + + res = cut_circuit(convert_input) + + assert res.shape == (shots, 2) + assert isinstance(res, type(convert_input)) + + @pytest.mark.parametrize( + "interface_import,interface", + [ + ("autograd.numpy", "autograd"), + ("tensorflow", "tensorflow"), + ("torch", "torch"), + ("jax.numpy", "jax"), + ], + ) + def test_all_interfaces_mc(self, interface_import, interface): + """ + Tests that `cut_circuit_mc` returns the correct type of expectation + value output in all interfaces + """ + lib = pytest.importorskip(interface_import) + + shots = 10 + dev = qml.device("default.qubit", wires=2, shots=shots) + + @qml.cut_circuit_mc(fn) + @qml.qnode(dev, interface=interface) + def cut_circuit(x): + qml.RX(x, wires=0) + qml.RY(0.5, wires=1) + qml.RX(1.3, wires=2) + + qml.CNOT(wires=[0, 1]) + qml.WireCut(wires=1) + qml.CNOT(wires=[1, 2]) + + qml.RX(x, wires=0) + qml.RY(0.7, wires=1) + qml.RX(2.3, wires=2) + return qml.sample(wires=[0, 2]) + + v = 0.319 + convert_input = qml.math.convert_like(v, lib.ones(1)) + res = cut_circuit(convert_input) + + assert isinstance(res, type(convert_input)) + + def test_mc_with_mid_circuit_measurement(self, mocker): + """Tests the full sample-based circuit cutting pipeline successfully returns a + single value for a circuit that contains mid-circuit + measurements and terminal sample measurements using the `cut_circuit_mc` + transform.""" + + shots = 10 + dev = qml.device("default.qubit", wires=3, shots=shots) + + @qml.cut_circuit_mc(fn) + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=0) + qml.CNOT(wires=[0, 1]) + qml.WireCut(wires=1) + qml.RX(np.sin(x) ** 2, wires=1) + qml.CNOT(wires=[1, 2]) + qml.WireCut(wires=1) + qml.CNOT(wires=[0, 1]) + return qml.sample(wires=[0, 1]) + + spy = mocker.spy(qcut, "qcut_processing_fn_mc") + x = np.array(0.531, requires_grad=True) + res = circuit(x) + + spy.assert_called_once() + assert res.size == 1 + + def test_mc_circuit_with_disconnected_components(self, mocker): + """Tests if a sample-based circuit that is fragmented into subcircuits such + that some of the subcircuits are disconnected from the final terminal sample + measurements is executed successfully""" + shots = 10 + dev = qml.device("default.qubit", wires=3, shots=shots) + + @qml.cut_circuit_mc(fn) + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=0) + qml.CNOT(wires=[0, 1]) + qml.WireCut(wires=1) + qml.CNOT(wires=[1, 2]) + qml.RY(x**2, wires=2) + return qml.sample(wires=[0, 1]) + + x = 0.4 + res = circuit(x) + assert res.size == 1 + + def test_mc_circuit_with_trivial_wire_cut(self, mocker): + """Tests that a sample-based circuit with a trivial wire cut (not + separating the circuit into fragments) is executed successfully""" + shots = 10 + dev = qml.device("default.qubit", wires=2, shots=shots) + + @qml.cut_circuit_mc(fn) + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=0) + qml.CNOT(wires=[0, 1]) + qml.WireCut(wires=0) + qml.CNOT(wires=[0, 1]) + return qml.sample(wires=[0, 1]) + + spy = mocker.spy(qcut, "qcut_processing_fn_mc") + + x = 0.4 + res = circuit(x) + assert res.size == 1 + assert len(spy.call_args[0][0]) == shots + assert len(spy.call_args[0]) == 1 + + def test_mc_complicated_circuit(self, mocker): + """ + Tests that the full sample-based circuit cutting pipeline successfully returns a + value for a complex circuit with multiple wire cut scenarios. The circuit is cut into + fragments of at most 2 qubits and is drawn below: + + 0: ──X──//─╭C───//──RX─╭C───//─╭C──╭//────────────────────┤ + 1: ────────╰X──────────╰X───//─╰Z──╰//────────────────╭RX─┤ ╭Sample + 2: ──H─╭C──────────────────────╭RY────────────────╭RY─│───┤ ├Sample + 3: ────╰RY──//──H──╭C───//──H──╰C───//─╭RY──//──H─╰C──│───┤ ╰Sample + 4: ────────────────╰RY──H──────────────╰C─────────────╰C──┤ + """ + + # We need a 4-qubit device to account for mid-circuit measurements + shots = 10 + dev = qml.device("default.qubit", wires=4, shots=shots) + + def two_qubit_unitary(param, wires): + qml.Hadamard(wires=[wires[0]]) + qml.CRY(param, wires=[wires[0], wires[1]]) + + @qml.cut_circuit_mc(fn) + @qml.qnode(dev) + def circuit(params): + qml.BasisState(np.array([1]), wires=[0]) + qml.WireCut(wires=0) + + qml.CNOT(wires=[0, 1]) + qml.WireCut(wires=0) + qml.RX(params[0], wires=0) + qml.CNOT(wires=[0, 1]) + + qml.WireCut(wires=0) + qml.WireCut(wires=1) + + qml.CZ(wires=[0, 1]) + qml.WireCut(wires=[0, 1]) + + two_qubit_unitary(params[1], wires=[2, 3]) + qml.WireCut(wires=3) + two_qubit_unitary(params[2] ** 2, wires=[3, 4]) + qml.WireCut(wires=3) + two_qubit_unitary(np.sin(params[3]), wires=[3, 2]) + qml.WireCut(wires=3) + two_qubit_unitary(np.sqrt(params[4]), wires=[4, 3]) + qml.WireCut(wires=3) + two_qubit_unitary(np.cos(params[1]), wires=[3, 2]) + qml.CRX(params[2], wires=[4, 1]) + + return qml.sample(wires=[1, 2, 3]) + + spy = mocker.spy(qcut, "qcut_processing_fn_mc") + + params = np.array([0.4, 0.5, 0.6, 0.7, 0.8], requires_grad=True) + res = circuit(params) + + spy.assert_called_once() + assert res.size == 1 + class TestContractTensors: """Tests for the contract_tensors function"""