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

Implement wire cutting as a two-qubit instruction #174

Merged
merged 15 commits into from
Jul 29, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions circuit_knitting/cutting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

PartitionedCuttingProblem
CuttingExperimentResults
instructions.Move

Quasi-Probability Decomposition (QPD)
=====================================
Expand Down
18 changes: 18 additions & 0 deletions circuit_knitting/cutting/instructions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# This code is a Qiskit project.
garrison marked this conversation as resolved.
Show resolved Hide resolved

# (C) Copyright IBM 2023.

# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

r"""Quantum circuit :class:`~qiskit.Instruction`\ s useful for circuit cutting."""

from .move import Move

__all__ = [
"Move",
]
82 changes: 82 additions & 0 deletions circuit_knitting/cutting/instructions/move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Two-qubit instruction representing a swap + single-qubit reset."""
from __future__ import annotations

from qiskit.circuit import QuantumCircuit, Instruction


class Move(Instruction):
"""A two-qubit instruction representing a reset of the second qubit followed by a swap.

**Circuit Symbol:**

.. parsed-literal::

┌───────┐
q_0: ┤0 ├ q_0: ──────X─
│ Move │ = │
q_1: ┤1 ├ q_1: ─|0>──X─
└───────┘

The desired effect of this instruction, typically, is to move the state of
the first qubit to the second qubit. For this to work as expected, the
second incoming qubit must share no entanglement with the remainder of the
system. If this qubit *is* entangled, then performing the reset operation
will in turn implement a quantum channel on the other qubit(s) with which
it is entangled, resulting in the partial collapse of those qubits.

The simplest way to ensure that the second (i.e., destination) qubit shares
no entanglement with the remainder of the system is to use a fresh qubit
which has not been used since initialization.

Another valid way is to use, as a desination qubit, a qubit whose immediate
prior use was as the source (i.e., first) qubit of a preceding
:class:`Move` operation.

The following circuit contains two :class:`Move` operations, corresponding
to each of the aforementioned cases:

.. plot::
:include-source:

import numpy as np
from qiskit import QuantumCircuit
from circuit_knitting.cutting.instructions import Move

qc = QuantumCircuit(4)
qc.ryy(np.pi / 4, 0, 1)
qc.rx(np.pi / 4, 3)
qc.append(Move(), [1, 2])
qc.rz(np.pi / 4, 0)
qc.ryy(np.pi / 4, 2, 3)
qc.append(Move(), [2, 1])
qc.ryy(np.pi / 4, 0, 1)
qc.rx(np.pi / 4, 3)
qc.draw("mpl")

A full demonstration of the :class:`Move` instruction is available in `the
introductory tutorial on wire cutting
<../circuit_cutting/tutorials/03_wire_cutting_via_move_instruction.ipynb>`__.
"""

def __init__(self, label: str | None = None):
"""Create a :class:`Move` instruction."""
super().__init__("move", 2, 0, [], label=label)

def _define(self):
"""Set definition to equivalent circuit."""
qc = QuantumCircuit(2, name=self.name)
qc.reset(1)
qc.swap(0, 1)
self.definition = qc
2 changes: 1 addition & 1 deletion circuit_knitting/cutting/qpd/instructions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

r"""Quantum circuit :class:`~qiskit.Instruction`\ s for repesenting quasiprobability decompositions."""
r"""Quantum circuit :class:`~qiskit.Instruction`\ s for representing quasiprobability decompositions."""

from .qpd_gate import BaseQPDGate, SingleQubitQPDGate, TwoQubitQPDGate
from .qpd_measure import QPDMeasure
Expand Down
49 changes: 42 additions & 7 deletions circuit_knitting/cutting/qpd/qpd.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
from qiskit.circuit import (
QuantumCircuit,
Gate,
Instruction,
ClassicalRegister,
CircuitInstruction,
Measure,
Reset,
)
from qiskit.circuit.library.standard_gates import (
XGate,
Expand Down Expand Up @@ -68,6 +70,7 @@

from .qpd_basis import QPDBasis
from .instructions import BaseQPDGate, TwoQubitQPDGate, QPDMeasure
from ..instructions import Move
from ...utils.iteration import unique_by_id, strict_zip


Expand Down Expand Up @@ -543,7 +546,7 @@ def decompose_qpd_instructions(
return new_qc


_qpdbasis_from_gate_funcs: dict[str, Callable[[Gate], QPDBasis]] = {}
_qpdbasis_from_gate_funcs: dict[str, Callable[[Instruction], QPDBasis]] = {}


def _register_qpdbasis_from_gate(*args):
Expand All @@ -555,7 +558,7 @@ def g(f):
return g


def qpdbasis_from_gate(gate: Gate) -> QPDBasis:
def qpdbasis_from_gate(gate: Instruction) -> QPDBasis:
"""
Generate a :class:`.QPDBasis` object, given a supported operation.

Expand All @@ -564,12 +567,15 @@ def qpdbasis_from_gate(gate: Gate) -> QPDBasis:
parameters, but there are some special cases (see, e.g., `qiskit issue #10396
<https://github.com/Qiskit/qiskit-terra/issues/10396>`__).

The :class:`.Move` operation, which can be used to specify a wire cut,
is also supported.

Returns:
The newly-instantiated :class:`QPDBasis` object

Raises:
ValueError: Gate not supported.
ValueError: Cannot decompose gate with unbound parameters.
ValueError: Instruction not supported.
ValueError: Cannot decompose instruction with unbound parameters.
ValueError: ``to_matrix`` conversion of two-qubit gate failed.
"""
try:
Expand Down Expand Up @@ -598,10 +604,10 @@ def qpdbasis_from_gate(gate: Gate) -> QPDBasis:
operations.append(UnitaryGate(d.K1l))
return retval

raise ValueError(f"Gate not supported: {gate.name}")
raise ValueError(f"Instruction not supported: {gate.name}")


def _explicitly_supported_gates() -> set[str]:
def _explicitly_supported_instructions() -> set[str]:
"""
Return a set of instruction names with explicit support for automatic decomposition.

Expand Down Expand Up @@ -977,12 +983,41 @@ def _theta_from_gate(gate: Gate) -> float:
theta = float(gate.params[0])
except TypeError as err:
raise ValueError(
f"Cannot decompose ({gate.name}) gate with unbound parameters."
f"Cannot decompose ({gate.name}) instruction with unbound parameters."
) from err

return theta


@_register_qpdbasis_from_gate("move")
def _(gate: Move):
i_measurement = [Reset()]
x_measurement = [HGate(), QPDMeasure(), Reset()]
y_measurement = [SdgGate(), HGate(), QPDMeasure(), Reset()]
z_measurement = [QPDMeasure(), Reset()]

prep_0 = [Reset()]
prep_1 = [Reset(), XGate()]
prep_plus = [Reset(), HGate()]
prep_minus = [Reset(), XGate(), HGate()]
prep_iplus = [Reset(), HGate(), SGate()]
prep_iminus = [Reset(), XGate(), HGate(), SGate()]

# https://arxiv.org/abs/1904.00102v2 Eqs. (12)-(19)
maps1, maps2, coeffs = zip(
(i_measurement, prep_0, 0.5),
(i_measurement, prep_1, 0.5),
(x_measurement, prep_plus, 0.5),
(x_measurement, prep_minus, -0.5),
(y_measurement, prep_iplus, 0.5),
(y_measurement, prep_iminus, -0.5),
(z_measurement, prep_0, 0.5),
(z_measurement, prep_1, -0.5),
)
maps = list(zip(maps1, maps2))
return QPDBasis(maps, coeffs)


def _validate_qpd_instructions(
circuit: QuantumCircuit, instruction_ids: Sequence[Sequence[int]]
):
Expand Down
6 changes: 3 additions & 3 deletions circuit_knitting/cutting/qpd/qpd_basis.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from collections.abc import Sequence

import numpy as np
from qiskit.circuit import Gate, Instruction
from qiskit.circuit import Instruction


class QPDBasis:
Expand Down Expand Up @@ -114,15 +114,15 @@ def overhead(self) -> float:
return self._kappa**2

@staticmethod
def from_gate(gate: Gate) -> "QPDBasis":
def from_gate(gate: Instruction) -> QPDBasis:
"""
Generate a :class:`.QPDBasis` object, given a supported operation.

This static method is provided for convenience; it simply
calls :func:`~qpd.qpd.qpdbasis_to_gate` under the hood.

Args:
gate: The gate from which to instantiate a decomposition
gate: The instruction from which to instantiate a decomposition

Returns:
The newly-instantiated :class:`QPDBasis` object
Expand Down
1 change: 0 additions & 1 deletion docs/circuit_cutting/cutqc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ the new code. These features currently specific to ``cutqc`` include:

- Reconstruction of probability distributions (rather than expectation values)
- Automatic cut finding
- Wire cutting (rather than gate cutting)

.. _cutqc tutorials:

Expand Down
1 change: 0 additions & 1 deletion docs/circuit_cutting/explanation/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ Key terms

Current limitations
-------------------
* QPD-based wire cutting will be available no sooner than CKT v0.3.0. The `cutqc <../cutqc/index.rst>`__ package may be used for wire cutting in the meantime.
* ``PauliList`` is the only supported observable format until no sooner than CKT v0.3.0.

References
Expand Down

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/circuit_cutting/tutorials/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ Circuit Cutting Tutorials
subexperiments for each qubit partition in parallel.
- `Tutorial 2 <02_gate_cutting_to_reduce_circuit_depth.ipynb>`__:
Cut gates requiring many SWAPs to decrease circuit depth.
- `Tutorial 3 <03_wire_cutting_via_move_instruction.ipynb>`__:
Specify wire cuts as a two-qubit :class:`.Move` operation.
5 changes: 5 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"sphinx.ext.mathjax",
"sphinx.ext.viewcode",
"sphinx.ext.extlinks",
"matplotlib.sphinxext.plot_directive",
# "sphinx.ext.autosectionlabel",
"jupyter_sphinx",
"sphinx_autodoc_typehints",
Expand Down Expand Up @@ -78,6 +79,10 @@
"**/README.rst",
]

# matplotlib.sphinxext.plot_directive options
plot_html_show_formats = False
plot_formats = ["svg"]

# Redirects for pages that have moved
redirects = {
"circuit_cutting/tutorials/gate_cutting_to_reduce_circuit_width.html": "01_gate_cutting_to_reduce_circuit_width.html",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
features:
- |
The :mod:`circuit_knitting.cutting` module now supports wire
cutting. There is a :ref:`new tutorial <circuit cutting
tutorials>` that explains how to use it.
6 changes: 4 additions & 2 deletions test/cutting/qpd/test_qpd.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
_generate_exact_weights_and_conditional_probabilities,
_nonlocal_qpd_basis_from_u,
_u_from_thetavec,
_explicitly_supported_gates,
_explicitly_supported_instructions,
)


Expand Down Expand Up @@ -267,6 +267,7 @@ def test_decompose_qpd_instructions(self):
(SwapGate(), 7),
(iSwapGate(), 7),
(DCXGate(), 7),
(Move(), 4),
)
@unpack
def test_optimal_kappa_for_known_gates(self, instruction, gamma):
Expand Down Expand Up @@ -427,7 +428,7 @@ def from_theta(theta):
assert weights[map_ids][1] == WeightType.SAMPLED

def test_explicitly_supported_gates(self):
gates = _explicitly_supported_gates()
gates = _explicitly_supported_instructions()
self.assertEqual(
{
"rxx",
Expand All @@ -448,6 +449,7 @@ def test_explicitly_supported_gates(self):
"swap",
"iswap",
"dcx",
"move",
},
gates,
)
Expand Down
4 changes: 2 additions & 2 deletions test/cutting/qpd/test_qpd_basis.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def test_eq(self):
def test_unsupported_gate(self):
with pytest.raises(ValueError) as e_info:
QPDBasis.from_gate(C3XGate())
assert e_info.value.args[0] == "Gate not supported: mcx"
assert e_info.value.args[0] == "Instruction not supported: mcx"

def test_unbound_parameter(self):
with self.subTest("Explicitly supported gate"):
Expand All @@ -131,7 +131,7 @@ def test_unbound_parameter(self):
QPDBasis.from_gate(RZZGate(Parameter("θ")))
assert (
e_info.value.args[0]
== "Cannot decompose (rzz) gate with unbound parameters."
== "Cannot decompose (rzz) instruction with unbound parameters."
)
with self.subTest("Implicitly supported gate"):
# For implicitly supported gates, we can detect that `to_matrix`
Expand Down
Loading