diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index e06ed2adfc9..3dd480dbd63 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -20,7 +20,10 @@ `ControlledOperation`. Control values of `0` are implemented by `qml.PauliX` applied before and after the controlled operation [(#2288)](https://github.com/PennyLaneAI/pennylane/pull/2288) - + +* Circuit cutting now performs expansion to search for wire cuts in contained operations or tapes. + [(#2340)](https://github.com/PennyLaneAI/pennylane/pull/2340) +

Deprecations

Breaking changes

@@ -48,4 +51,4 @@ This release contains contributions from (in alphabetical order): -Karim Alaa El-Din, Anthony Hayes, Josh Izaac, Christina Lee. \ No newline at end of file +Karim Alaa El-Din, Thomas Bromley, Anthony Hayes, Josh Izaac, Christina Lee. diff --git a/pennylane/transforms/qcut.py b/pennylane/transforms/qcut.py index 8c3e6904216..5c3c7bfec0f 100644 --- a/pennylane/transforms/qcut.py +++ b/pennylane/transforms/qcut.py @@ -940,7 +940,10 @@ def qcut_processing_fn( @batch_transform def cut_circuit( - tape: QuantumTape, use_opt_einsum: bool = False, device_wires: Optional[Wires] = None + tape: QuantumTape, + use_opt_einsum: bool = False, + device_wires: Optional[Wires] = None, + max_depth: int = 1, ) -> Tuple[Tuple[QuantumTape], Callable]: """ Cut up a quantum circuit into smaller circuit fragments. @@ -965,6 +968,7 @@ def cut_circuit( e.g., ``pip install opt_einsum``. Both settings for ``use_opt_einsum`` result in a differentiable contraction. device_wires (Wires): wires of the device that the cut circuits are to be run on + max_depth (int): the maximum depth used to expand the circuit while searching for wire cuts Returns: Tuple[Tuple[QuantumTape], Callable]: the tapes corresponding to the circuit fragments as a @@ -1155,6 +1159,7 @@ def circuit(x): ... ) 0.47165198882111165 """ + # pylint: disable=unused-argument if len(tape.measurements) != 1: raise ValueError( "The circuit cutting workflow only supports circuits with a single output " @@ -1177,10 +1182,6 @@ def circuit(x): "installed using:\npip install opt_einsum" ) from e - num_cut = len([op for op in tape.operations if isinstance(op, WireCut)]) - if num_cut == 0: - raise ValueError("Cannot apply the circuit cutting workflow to a circuit without any cuts") - g = tape_to_graph(tape) replace_wire_cut_nodes(g) fragments, communication_graph = fragment_graph(g) @@ -1216,6 +1217,33 @@ def qnode_execution_wrapper(self, qnode, targs, tkwargs): return self.default_qnode_wrapper(qnode, targs, tkwargs) +def _qcut_expand_fn( + tape: QuantumTape, + use_opt_einsum: bool = False, + device_wires: Optional[Wires] = None, + max_depth: int = 1, +): + """Expansion function for circuit cutting. + + Expands operations until reaching a depth that includes :class:`~.WireCut` operations. + """ + # pylint: disable=unused-argument + for op in tape.operations: + if isinstance(op, WireCut): + return tape + + if max_depth > 0: + return cut_circuit.expand_fn(tape.expand(), max_depth=max_depth - 1) + + raise ValueError( + "No WireCut operations found in the circuit. Consider increasing the max_depth value if " + "operations or nested tapes contain WireCut operations." + ) + + +cut_circuit.expand_fn = _qcut_expand_fn + + def remap_tape_wires(tape: QuantumTape, wires: Sequence) -> QuantumTape: """Map the wires of a tape to a new set of wires. diff --git a/tests/transforms/test_qcut.py b/tests/transforms/test_qcut.py index b78dc630966..a37a3813504 100644 --- a/tests/transforms/test_qcut.py +++ b/tests/transforms/test_qcut.py @@ -2691,6 +2691,7 @@ def test_multiple_measurements_raises(self): to be cut""" with qml.tape.QuantumTape() as tape: + qml.WireCut(wires=0) qml.expval(qml.PauliZ(0)) qml.expval(qml.PauliZ(1)) @@ -2700,14 +2701,18 @@ def test_multiple_measurements_raises(self): def test_no_measurements_raises(self): """Tests if a ValueError is raised when a tape with no measurement is requested to be cut""" + with qml.tape.QuantumTape() as tape: + qml.WireCut(wires=0) + with pytest.raises(ValueError, match="The circuit cutting workflow only supports circuits"): - qcut.cut_circuit(qml.tape.QuantumTape()) + qcut.cut_circuit(tape) def test_non_expectation_raises(self): """Tests if a ValueError is raised when a tape with measurements that are not expectation values is requested to be cut""" with qml.tape.QuantumTape() as tape: + qml.WireCut(wires=0) qml.var(qml.PauliZ(0)) with pytest.raises(ValueError, match="workflow only supports circuits with expectation"): @@ -2716,6 +2721,7 @@ def test_non_expectation_raises(self): def test_fail_import(self, monkeypatch): """Test if an ImportError is raised when opt_einsum is requested but not installed""" with qml.tape.QuantumTape() as tape: + qml.WireCut(wires=0) qml.expval(qml.PauliZ(0)) with monkeypatch.context() as m: @@ -2730,10 +2736,85 @@ def test_no_cuts_raises(self): with qml.tape.QuantumTape() as tape: qml.expval(qml.PauliZ(0)) - with pytest.raises(ValueError, match="to a circuit without any cuts"): + with pytest.raises(ValueError, match="No WireCut operations found in the circuit."): qcut.cut_circuit(tape) +class TestCutCircuitExpansion: + """Test of expansion in the cut_circuit function""" + + def test_no_expansion(self, mocker): + """Test if no/trivial expansion occurs if WireCut operations are already present in the + tape""" + with qml.tape.QuantumTape() as tape: + qml.RX(0.3, wires=0) + qml.WireCut(wires=0) + qml.RY(0.4, wires=0) + qml.expval(qml.PauliZ(0)) + + spy = mocker.spy(qcut.cut_circuit, "expand_fn") + qcut.cut_circuit(tape, device_wires=[0]) + spy.assert_called_once() + + def test_expansion(self, mocker): + """Test if expansion occurs if WireCut operations are present in a nested tape""" + with qml.tape.QuantumTape() as tape: + qml.RX(0.3, wires=0) + with qml.tape.QuantumTape() as _: + qml.WireCut(wires=0) + qml.RY(0.4, wires=0) + qml.expval(qml.PauliZ(0)) + + spy = mocker.spy(qcut.cut_circuit, "expand_fn") + qcut.cut_circuit(tape, device_wires=[0]) + + assert spy.call_count == 2 + + def test_expansion_error(self): + """Test if a ValueError is raised if expansion continues beyond the maximum depth""" + with qml.tape.QuantumTape() as tape: + qml.RX(0.3, wires=0) + with qml.tape.QuantumTape() as _: + with qml.tape.QuantumTape() as __: + qml.WireCut(wires=0) + qml.RY(0.4, wires=0) + qml.expval(qml.PauliZ(0)) + + with pytest.raises(ValueError, match="No WireCut operations found in the circuit."): + qcut.cut_circuit(tape, device_wires=[0], max_depth=1) + + def test_expansion_ttn(self, mocker): + """Test if wire cutting is compatible with the tree tensor network operation""" + + def block(weights, wires): + qml.CNOT(wires=[wires[0], wires[1]]) + qml.RY(weights[0], wires=wires[0]) + qml.RY(weights[1], wires=wires[1]) + qml.WireCut(wires=wires[1]) + + n_wires = 4 + n_block_wires = 2 + n_params_block = 2 + n_blocks = qml.TTN.get_n_blocks(range(n_wires), n_block_wires) + template_weights = [[0.1, -0.3]] * n_blocks + + dev_cut = qml.device("default.qubit", wires=2) + dev_big = qml.device("default.qubit", wires=4) + + def circuit(template_weights): + qml.TTN(range(n_wires), n_block_wires, block, n_params_block, template_weights) + return qml.expval(qml.PauliZ(wires=n_wires - 1)) + + qnode = qml.QNode(circuit, dev_big) + qnode_cut = qcut.cut_circuit(qml.QNode(circuit, dev_cut)) + + spy = mocker.spy(qcut.cut_circuit, "expand_fn") + res = qnode_cut(template_weights) + assert spy.call_count == 2 + + assert np.isclose(res, qnode(template_weights)) + + class TestCutStrategy: """Tests for class CutStrategy"""