Skip to content

Commit

Permalink
Add support for SwapGate, iSwapGate, and DCXGate (#294)
Browse files Browse the repository at this point in the history
* Add support for `SwapGate`

* Reorder terms

[ci skip]

* Add missing terms

* DRY the coefficients

* Fix coverage

* Add support for `iSwapGate`

* Fix black

* Add to release note

* Fix type hint

* Gates without parameters are nicer to work with

and can be singletons, one day!

* Remove a line

* Add comments describing channels

* `_copy_unique_sublists`

* Add `DCXGate`

* Tweak

* Add `DCXGate` to release note

* Additional tests

* Update docstring with supported gates
  • Loading branch information
garrison authored Jul 5, 2023
1 parent 24cac08 commit b76f9c6
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 7 deletions.
161 changes: 159 additions & 2 deletions circuit_knitting/cutting/qpd/qpd.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@
ZGate,
HGate,
SGate,
TGate,
SdgGate,
SXGate,
SXdgGate,
TGate,
RXGate,
RYGate,
RZGate,
Expand All @@ -45,15 +46,18 @@
CHGate,
CSGate,
CSdgGate,
CSXGate,
RXXGate,
RYYGate,
RZZGate,
CRXGate,
CRYGate,
CRZGate,
ECRGate,
CSXGate,
CPhaseGate,
SwapGate,
iSwapGate,
DCXGate,
)

from .qpd_basis import QPDBasis
Expand Down Expand Up @@ -216,6 +220,9 @@ def qpdbasis_from_gate(gate: Gate) -> QPDBasis:
- :class:`~qiskit.circuit.library.CSGate`
- :class:`~qiskit.circuit.library.CSdgGate`
- :class:`~qiskit.circuit.library.CPhaseGate`
- :class:`~qiskit.circuit.library.SwapGate`
- :class:`~qiskit.circuit.library.iSwapGate`
- :class:`~qiskit.circuit.library.DCXGate`
The above gate names can also be determined by calling
:func:`supported_gates`.
Expand Down Expand Up @@ -244,6 +251,156 @@ def supported_gates() -> set[str]:
return set(_qpdbasis_from_gate_funcs)


def _copy_unique_sublists(lsts: tuple[list, ...], /) -> tuple[list, ...]:
"""
Copy each list in a sequence of lists while preserving uniqueness.
This is useful to ensure that the two sets of ``maps`` in a
:class:`QPDBasis` will be independent of each other. This enables one to
subsequently edit the ``maps`` independently of each other (e.g., to apply
single-qubit pre- or post-rotations.
"""
copy_by_id: dict[int, list] = {}
for lst in lsts:
if id(lst) not in copy_by_id:
copy_by_id[id(lst)] = lst.copy()
return tuple(copy_by_id[id(lst)] for lst in lsts)


def _nonlocal_qpd_basis_from_u(
u: np.typing.NDArray[np.complex128] | Sequence[complex], /
) -> QPDBasis:
u = np.asarray(u)
if u.shape != (4,):
raise ValueError(
f"u vector has wrong shape: {u.shape} (1D vector of length 4 expected)"
)
# The following operations are described in Sec. 2.3 of
# https://quantum-journal.org/papers/q-2021-01-28-388/
#
# Projective measurements in each basis
A0x = [HGate(), QPDMeasure(), HGate()]
A0y = [SdgGate(), HGate(), QPDMeasure(), HGate(), SGate()]
A0z = [QPDMeasure()]
# Single qubit rotations that swap two axes. There are "plus" and "minus"
# versions of these rotations. The "minus" rotations also flip the sign
# along that axis.
Axyp = [SGate(), YGate()]
Axym = [ZGate()] + Axyp
Ayzp = [SXGate(), ZGate()]
Ayzm = [XGate()] + Ayzp
Azxp = [HGate()]
Azxm = [YGate()] + Azxp
# Single qubit rotations by ±pi/4 about each axis.
B0xp = [SXGate()]
B0xm = [SXdgGate()]
B0yp = [RYGate(0.5 * np.pi)]
B0ym = [RYGate(-0.5 * np.pi)]
B0zp = [SGate()]
B0zm = [SdgGate()]
# Projective measurements, each followed by the proper flip.
Bxy = A0z + [XGate()]
Byz = A0x + [YGate()]
Bzx = A0y + [ZGate()]
# The following values occur repeatedly in the coefficients
uu01 = u[0] * np.conj(u[1])
uu02 = u[0] * np.conj(u[2])
uu03 = u[0] * np.conj(u[3])
uu12 = u[1] * np.conj(u[2])
uu23 = u[2] * np.conj(u[3])
uu31 = u[3] * np.conj(u[1])
coeffs, maps1, maps2 = zip(
# First line of Eq. (19) in
# https://quantum-journal.org/papers/q-2021-01-28-388/
(np.abs(u[0]) ** 2, [], []), # Identity
(np.abs(u[1]) ** 2, [XGate()], [XGate()]),
(np.abs(u[2]) ** 2, [YGate()], [YGate()]),
(np.abs(u[3]) ** 2, [ZGate()], [ZGate()]),
# Second line
(2 * np.real(uu01), A0x, A0x),
(2 * np.real(uu02), A0y, A0y),
(2 * np.real(uu03), A0z, A0z),
(0.5 * np.real(uu12), Axyp, Axyp),
(-0.5 * np.real(uu12), Axyp, Axym),
(-0.5 * np.real(uu12), Axym, Axyp),
(0.5 * np.real(uu12), Axym, Axym),
(0.5 * np.real(uu23), Ayzp, Ayzp),
(-0.5 * np.real(uu23), Ayzp, Ayzm),
(-0.5 * np.real(uu23), Ayzm, Ayzp),
(0.5 * np.real(uu23), Ayzm, Ayzm),
(0.5 * np.real(uu31), Azxp, Azxp),
(-0.5 * np.real(uu31), Azxp, Azxm),
(-0.5 * np.real(uu31), Azxm, Azxp),
(0.5 * np.real(uu31), Azxm, Azxm),
(-0.5 * np.real(uu01), B0xp, B0xp),
(0.5 * np.real(uu01), B0xp, B0xm),
(0.5 * np.real(uu01), B0xm, B0xp),
(-0.5 * np.real(uu01), B0xm, B0xm),
(-0.5 * np.real(uu02), B0yp, B0yp),
(0.5 * np.real(uu02), B0yp, B0ym),
(0.5 * np.real(uu02), B0ym, B0yp),
(-0.5 * np.real(uu02), B0ym, B0ym),
(-0.5 * np.real(uu03), B0zp, B0zp),
(0.5 * np.real(uu03), B0zp, B0zm),
(0.5 * np.real(uu03), B0zm, B0zp),
(-0.5 * np.real(uu03), B0zm, B0zm),
(-2 * np.real(uu12), Bxy, Bxy),
(-2 * np.real(uu23), Byz, Byz),
(-2 * np.real(uu31), Bzx, Bzx),
# Third line
(np.imag(uu01), A0x, B0xp),
(-np.imag(uu01), A0x, B0xm),
(np.imag(uu01), B0xp, A0x),
(-np.imag(uu01), B0xm, A0x),
(np.imag(uu02), A0y, B0yp),
(-np.imag(uu02), A0y, B0ym),
(np.imag(uu02), B0yp, A0y),
(-np.imag(uu02), B0ym, A0y),
(np.imag(uu03), A0z, B0zp),
(-np.imag(uu03), A0z, B0zm),
(np.imag(uu03), B0zp, A0z),
(-np.imag(uu03), B0zm, A0z),
(np.imag(uu12), Axyp, Bxy),
(-np.imag(uu12), Axym, Bxy),
(np.imag(uu12), Bxy, Axyp),
(-np.imag(uu12), Bxy, Axym),
(np.imag(uu23), Ayzp, Byz),
(-np.imag(uu23), Ayzm, Byz),
(np.imag(uu23), Byz, Ayzp),
(-np.imag(uu23), Byz, Ayzm),
(np.imag(uu31), Azxp, Bzx),
(-np.imag(uu31), Azxm, Bzx),
(np.imag(uu31), Bzx, Azxp),
(-np.imag(uu31), Bzx, Azxm),
)
maps = list(zip(maps1, _copy_unique_sublists(maps2)))
return QPDBasis(maps, coeffs)


@_register_qpdbasis_from_gate("swap")
def _(gate: SwapGate):
return _nonlocal_qpd_basis_from_u([(1 + 1j) / np.sqrt(8)] * 4)


@_register_qpdbasis_from_gate("iswap")
def _(gate: iSwapGate):
return _nonlocal_qpd_basis_from_u([0.5, 0.5j, 0.5j, 0.5])


@_register_qpdbasis_from_gate("dcx")
def _(gate: DCXGate):
retval = qpdbasis_from_gate(iSwapGate())
# Modify basis according to DCXGate definition in Qiskit circuit library
# https://github.com/Qiskit/qiskit-terra/blob/e9f8b7c50968501e019d0cb426676ac606eb5a10/qiskit/circuit/library/standard_gates/equivalence_library.py#L938-L944
for operations in unique_by_id(m[0] for m in retval.maps):
operations.insert(0, SdgGate())
operations.insert(0, HGate())
for operations in unique_by_id(m[1] for m in retval.maps):
operations.insert(0, SdgGate())
operations.append(HGate())
return retval


@_register_qpdbasis_from_gate("rxx", "ryy", "rzz", "crx", "cry", "crz")
def _(gate: RXXGate | RYYGate | RZZGate | CRXGate | CRYGate | CRZGate):
# Constructing a virtual two-qubit gate by sampling single-qubit operations - Mitarai et al
Expand Down
3 changes: 3 additions & 0 deletions releasenotes/notes/additional-gates-f4ed6c0e8dc3a9be.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ features:
- :class:`~qiskit.circuit.library.CSdgGate`
- :class:`~qiskit.circuit.library.CPhaseGate`
- :class:`~qiskit.circuit.library.ECRGate`
- :class:`~qiskit.circuit.library.SwapGate`
- :class:`~qiskit.circuit.library.iSwapGate`
- :class:`~qiskit.circuit.library.DCXGate`
22 changes: 22 additions & 0 deletions test/cutting/qpd/test_qpd.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
generate_qpd_samples,
)
from circuit_knitting.cutting.qpd.qpd import *
from circuit_knitting.cutting.qpd.qpd import _nonlocal_qpd_basis_from_u


@ddt
Expand Down Expand Up @@ -248,6 +249,13 @@ def test_decompose_qpd_instructions(self):
(RXXGate(np.pi / 7), 1 + 2 * np.abs(np.sin(np.pi / 7))),
(RYYGate(np.pi / 7), 1 + 2 * np.abs(np.sin(np.pi / 7))),
(RZZGate(np.pi / 7), 1 + 2 * np.abs(np.sin(np.pi / 7))),
(CPhaseGate(np.pi / 7), 1 + 2 * np.abs(np.sin(np.pi / 14))),
(CSGate(), 1 + np.sqrt(2)),
(CSdgGate(), 1 + np.sqrt(2)),
(CSXGate(), 1 + np.sqrt(2)),
(SwapGate(), 7),
(iSwapGate(), 7),
(DCXGate(), 7),
)
@unpack
def test_optimal_kappa_for_known_gates(self, instruction, gamma):
Expand All @@ -260,6 +268,8 @@ def test_optimal_kappa_for_known_gates(self, instruction, gamma):
(CRXGate(np.pi / 7), 5, 5),
(CRYGate(np.pi / 7), 5, 5),
(CRZGate(np.pi / 7), 5, 5),
(CPhaseGate(np.pi / 7), 5, 5),
(ECRGate(), 5, 5),
(CXGate(), 5, 5),
(CZGate(), 5, 5),
(RZZGate(0), 1, 1),
Expand Down Expand Up @@ -302,6 +312,18 @@ def test_supported_gates(self):
"csdg",
"cp",
"ecr",
"swap",
"iswap",
"dcx",
},
gates,
)

def test_nonlocal_qpd_basis_from_u(self):
with self.subTest("Invalid shape"):
with pytest.raises(ValueError) as e_info:
_nonlocal_qpd_basis_from_u([1, 2, 3])
assert (
e_info.value.args[0]
== "u vector has wrong shape: (3,) (1D vector of length 4 expected)"
)
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 @@ -120,8 +120,8 @@ def test_eq(self):

def test_unsupported_gate(self):
with pytest.raises(ValueError) as e_info:
QPDBasis.from_gate(SwapGate())
assert e_info.value.args[0] == "Gate not supported: swap"
QPDBasis.from_gate(XXMinusYYGate(0.1))
assert e_info.value.args[0] == "Gate not supported: xx_minus_yy"

def test_unbound_parameter(self):
with pytest.raises(ValueError) as e_info:
Expand Down
12 changes: 9 additions & 3 deletions test/cutting/test_cutting_roundtrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@
CZGate,
CSGate,
CSdgGate,
CSXGate,
CRXGate,
CRYGate,
CRZGate,
ECRGate,
CSXGate,
CPhaseGate,
ECRGate,
SwapGate,
iSwapGate,
DCXGate,
)
from qiskit.extensions import UnitaryGate
from qiskit.quantum_info import PauliList, random_unitary
Expand All @@ -55,6 +58,9 @@ def append_random_unitary(circuit: QuantumCircuit, qubits):

@pytest.fixture(
params=[
[SwapGate()],
[iSwapGate()],
[DCXGate()],
[CXGate()],
[CYGate()],
[CZGate()],
Expand Down Expand Up @@ -133,7 +139,7 @@ def test_cutting_exact_reconstruction(example_circuit):
quasi_dists, coefficients = execute_experiments(
circuits=subcircuits,
subobservables=subobservables,
num_samples=1500,
num_samples=np.inf,
samplers=sampler,
)
simulated_expvals = reconstruct_expectation_values(
Expand Down

0 comments on commit b76f9c6

Please sign in to comment.