From 0a23b2f57c3feff6048af40e71039636b7ef271e Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 16 Mar 2022 08:15:01 -0400 Subject: [PATCH 01/11] Add expansion Co-authored-by: Josh Izaac --- pennylane/transforms/qcut.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/pennylane/transforms/qcut.py b/pennylane/transforms/qcut.py index 1e79a2c9353..0ce7b8bf36e 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 = 2, ) -> 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 " @@ -1216,6 +1221,31 @@ 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 _qcut_expand_fn(tape.expand(), max_depth=max_depth - 1) + + raise ValueError(f"Expanded tape {max_depth} times, no wirecuts found") + + +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. From 148c2409244decba0ae7eaadc5bbc101f58fa9f9 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 16 Mar 2022 08:16:50 -0400 Subject: [PATCH 02/11] Reword --- pennylane/transforms/qcut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/transforms/qcut.py b/pennylane/transforms/qcut.py index 0ce7b8bf36e..f052c2439d7 100644 --- a/pennylane/transforms/qcut.py +++ b/pennylane/transforms/qcut.py @@ -1240,7 +1240,7 @@ def _qcut_expand_fn( if max_depth > 0: return _qcut_expand_fn(tape.expand(), max_depth=max_depth - 1) - raise ValueError(f"Expanded tape {max_depth} times, no wirecuts found") + raise ValueError(f"Expanded tape {max_depth} times, no WireCut operations found") cut_circuit.expand_fn = _qcut_expand_fn From 558307b261ed79bcee1257984cb30a112977de81 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 16 Mar 2022 08:39:26 -0400 Subject: [PATCH 03/11] Reword raise --- pennylane/transforms/qcut.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pennylane/transforms/qcut.py b/pennylane/transforms/qcut.py index f052c2439d7..232cd8e5c53 100644 --- a/pennylane/transforms/qcut.py +++ b/pennylane/transforms/qcut.py @@ -1232,15 +1232,15 @@ def _qcut_expand_fn( 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 _qcut_expand_fn(tape.expand(), max_depth=max_depth - 1) + return cut_circuit.expand_fn(tape.expand(), max_depth=max_depth - 1) - raise ValueError(f"Expanded tape {max_depth} times, no WireCut operations found") + raise ValueError("No WireCut operations found in the expanded tape. Consider increasing " + "max_depth.") cut_circuit.expand_fn = _qcut_expand_fn From 80b87d2750e5cf9ba02bfccad9e977fd6b634f16 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 16 Mar 2022 08:43:24 -0400 Subject: [PATCH 04/11] Add tests --- tests/transforms/test_qcut.py | 75 +++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/transforms/test_qcut.py b/tests/transforms/test_qcut.py index fcca7338292..7125168c9ff 100644 --- a/tests/transforms/test_qcut.py +++ b/tests/transforms/test_qcut.py @@ -2734,6 +2734,81 @@ def test_no_cuts_raises(self): 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 expanded tape."): + 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""" From 55966d9aff85b7bc8e84f7839d93c341cb814621 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 16 Mar 2022 08:44:52 -0400 Subject: [PATCH 05/11] Add to changelog --- doc/releases/changelog-dev.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index b26a0cab2e5..4c9e49ec2d0 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -20,7 +20,9 @@ `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. +

Deprecations

Breaking changes

@@ -48,4 +50,4 @@ This release contains contributions from (in alphabetical order): -Karim Alaa El-Din, Guillermo Alonso-Linaje, Anthony Hayes, Josh Izaac +Karim Alaa El-Din, Guillermo Alonso-Linaje, Tom Bromley, Anthony Hayes, Josh Izaac From 30528057bab1c535b9c7444c6570cb60d9fd4123 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 16 Mar 2022 08:47:34 -0400 Subject: [PATCH 06/11] Update changelog --- doc/releases/changelog-dev.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 4c9e49ec2d0..e771823c5ef 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -22,6 +22,7 @@ [(#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

From 21189c6a0b0519d47fd9cddab793031fa79a3547 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 16 Mar 2022 08:48:13 -0400 Subject: [PATCH 07/11] Fix CI --- pennylane/transforms/qcut.py | 5 +++-- tests/transforms/test_qcut.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pennylane/transforms/qcut.py b/pennylane/transforms/qcut.py index 232cd8e5c53..9f624f692a0 100644 --- a/pennylane/transforms/qcut.py +++ b/pennylane/transforms/qcut.py @@ -1239,8 +1239,9 @@ def _qcut_expand_fn( if max_depth > 0: return cut_circuit.expand_fn(tape.expand(), max_depth=max_depth - 1) - raise ValueError("No WireCut operations found in the expanded tape. Consider increasing " - "max_depth.") + raise ValueError( + "No WireCut operations found in the expanded tape. Consider increasing " "max_depth." + ) cut_circuit.expand_fn = _qcut_expand_fn diff --git a/tests/transforms/test_qcut.py b/tests/transforms/test_qcut.py index 7125168c9ff..955189bed0c 100644 --- a/tests/transforms/test_qcut.py +++ b/tests/transforms/test_qcut.py @@ -2792,8 +2792,8 @@ def block(weights, wires): 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) + 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) From 2b53e0e1889353ecccff5fc565093df943f7db0b Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 16 Mar 2022 08:49:20 -0400 Subject: [PATCH 08/11] Update changelog --- doc/releases/changelog-dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index e771823c5ef..f6e7a40a987 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -51,4 +51,4 @@ This release contains contributions from (in alphabetical order): -Karim Alaa El-Din, Guillermo Alonso-Linaje, Tom Bromley, Anthony Hayes, Josh Izaac +Karim Alaa El-Din, Guillermo Alonso-Linaje, Thomas Bromley, Anthony Hayes, Josh Izaac From 94d9d2f2ba0144b107f277f61d5c0ddd6d8951c4 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 16 Mar 2022 08:50:16 -0400 Subject: [PATCH 09/11] Update max_depth default --- pennylane/transforms/qcut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/transforms/qcut.py b/pennylane/transforms/qcut.py index 9f624f692a0..696a6c8408c 100644 --- a/pennylane/transforms/qcut.py +++ b/pennylane/transforms/qcut.py @@ -943,7 +943,7 @@ def cut_circuit( tape: QuantumTape, use_opt_einsum: bool = False, device_wires: Optional[Wires] = None, - max_depth: int = 2, + max_depth: int = 1, ) -> Tuple[Tuple[QuantumTape], Callable]: """ Cut up a quantum circuit into smaller circuit fragments. From aaaf6bbdb27877087683d99fe6d755a442e427e6 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 16 Mar 2022 08:51:30 -0400 Subject: [PATCH 10/11] Fix --- pennylane/transforms/qcut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/transforms/qcut.py b/pennylane/transforms/qcut.py index 696a6c8408c..64ebd404953 100644 --- a/pennylane/transforms/qcut.py +++ b/pennylane/transforms/qcut.py @@ -1240,7 +1240,7 @@ def _qcut_expand_fn( return cut_circuit.expand_fn(tape.expand(), max_depth=max_depth - 1) raise ValueError( - "No WireCut operations found in the expanded tape. Consider increasing " "max_depth." + "No WireCut operations found in the expanded tape. Consider increasing max_depth." ) From 2e007cfc91ec9accc005e322ae53aa7bd77876ca Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 16 Mar 2022 09:34:01 -0400 Subject: [PATCH 11/11] Fix tests --- pennylane/transforms/qcut.py | 7 ++----- tests/transforms/test_qcut.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pennylane/transforms/qcut.py b/pennylane/transforms/qcut.py index 64ebd404953..1dd4cb1be08 100644 --- a/pennylane/transforms/qcut.py +++ b/pennylane/transforms/qcut.py @@ -1182,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) @@ -1240,7 +1236,8 @@ def _qcut_expand_fn( return cut_circuit.expand_fn(tape.expand(), max_depth=max_depth - 1) raise ValueError( - "No WireCut operations found in the expanded tape. Consider increasing max_depth." + "No WireCut operations found in the circuit. Consider increasing the max_depth value if " + "operations or nested tapes contain WireCut operations." ) diff --git a/tests/transforms/test_qcut.py b/tests/transforms/test_qcut.py index 955189bed0c..240ad0248cd 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,7 +2736,7 @@ 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) @@ -2774,7 +2780,7 @@ def test_expansion_error(self): qml.RY(0.4, wires=0) qml.expval(qml.PauliZ(0)) - with pytest.raises(ValueError, match="No WireCut operations found in the expanded tape."): + 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):