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

Add support for SwapGate, iSwapGate, and DCXGate #294

Merged
merged 21 commits into from
Jul 5, 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
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),
Copy link
Collaborator

@caleb-johnson caleb-johnson Jul 3, 2023

Choose a reason for hiding this comment

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

I'm a little confused why there are so many terms associated with lines 2 (and also 3) of eq 19. That summation should have 6 terms, and each of those terms looks like (uaua'* + ua'ua*)(Aaa' - Baa'). Can't quite figure out why there are 30 terms here

Copy link
Member Author

@garrison garrison Jul 3, 2023

Choose a reason for hiding this comment

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

The sum is over six possibilities, but on line two, there are actually two terms within the parentheses, so you'd expect 12 terms total. However, each boldface A or B operator actually is the sum (difference, really) of two channels [Eqs. (13) and (14)]. That's what the variables that end in p or m are about -- they're actually "plus" and "minus" versions of these channels. It turns out, we don't need to keep track of the "plus" and "minus" channels individually for ones that involve a mid-circuit measurement because QPDMeasure handles both cases, rather than thinking of them as two different projectors.

Everything above can be said for the third line, as well.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, I was just coming here to say I see that it's 6+12+12. Thanks! :)

(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`
garrison marked this conversation as resolved.
Show resolved Hide resolved
- :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