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"""