Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Circuit cutting: Add expansion to cut_circuit transform #2340

Merged
merged 12 commits into from
Mar 17, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<h3>Deprecations</h3>

<h3>Breaking changes</h3>
Expand Down Expand Up @@ -48,4 +51,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, Thomas Bromley, Anthony Hayes, Josh Izaac
38 changes: 33 additions & 5 deletions pennylane/transforms/qcut.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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 "
Expand All @@ -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)
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thrown by the indentation level here, it looks like we're defining the expand_fn method here but there some recursive subtlety here:
_qcut_expand_fn uses expand_fn which is defined by _qcut_expand_fn ...

Is this how it's working?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was thrown by this as well 🤔 It makes sense now that I re-read it, but I'm curious if the following would also work?

def _qcut_expand_fn(tape, ...)
    ...
    if max_depth > 0:
        return _qcut_expand_fn.expand_fn(tape.expand(), max_depth=max_depth - 1)

cut_circuit.expand_fn = _qcut_expand_fn

This is easier for me to grok on first read

Copy link
Contributor Author

@trbromley trbromley Mar 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes good question. It was originally

return _qcut_expand_fn(tape.expand(), max_depth=max_depth - 1)

on line 1236, and this works too. It just makes it hard to track with spies in the test, we'd need one spy for cut_circuit.expand_fn (first call) and another for _qcut_expand_fn (subsequent calls).



def remap_tape_wires(tape: QuantumTape, wires: Sequence) -> QuantumTape:
"""Map the wires of a tape to a new set of wires.

Expand Down
85 changes: 83 additions & 2 deletions tests/transforms/test_qcut.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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"):
Expand All @@ -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:
Expand All @@ -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"""

Expand Down