# This code is a Qiskit project.
# (C) Copyright IBM 2024.
# 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.
"""Instruction to :class:`.QPDBasis` decompositions."""
from __future__ import annotations
from collections.abc import Sequence, Callable
import numpy as np
from qiskit.circuit import (
Gate,
ControlledGate,
Instruction,
Reset,
)
from qiskit.circuit.library import UnitaryGate
from qiskit.circuit.library.standard_gates import (
XGate,
YGate,
ZGate,
HGate,
SGate,
SdgGate,
SXGate,
SXdgGate,
TGate,
TdgGate,
RXGate,
RYGate,
RZGate,
PhaseGate,
CXGate,
CYGate,
CZGate,
CHGate,
CSGate,
CSdgGate,
CSXGate,
RXXGate,
RYYGate,
RZZGate,
CRXGate,
CRYGate,
CRZGate,
ECRGate,
CPhaseGate,
SwapGate,
iSwapGate,
DCXGate,
)
from qiskit.synthesis import TwoQubitWeylDecomposition
from .qpd_basis import QPDBasis
from .instructions import QPDMeasure
from ..instructions import Move
from ..utils.iteration import unique_by_id
_qpdbasis_from_instruction_funcs: dict[str, Callable[[Instruction], QPDBasis]] = {}
def _register_qpdbasis_from_instruction(*args):
def g(f):
for name in args:
_qpdbasis_from_instruction_funcs[name] = f
return f
return g
[docs]
def qpdbasis_from_instruction(gate: Instruction, /) -> QPDBasis:
"""Generate a :class:`.QPDBasis` object, given a supported operation.
All two-qubit gates which implement the :meth:`~qiskit.circuit.Gate.to_matrix` method are
supported. This should include the vast majority of gates with no unbound
parameters, but there are some special cases (see, e.g., `qiskit issue #10396
<https://github.com/Qiskit/qiskit/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: Instruction not supported.
ValueError: Cannot decompose instruction with unbound parameters.
ValueError: ``to_matrix`` conversion of two-qubit gate failed.
"""
try:
f = _qpdbasis_from_instruction_funcs[gate.name]
except KeyError:
pass
else:
return f(gate)
if isinstance(gate, Gate) and gate.num_qubits == 2:
try:
mat = gate.to_matrix()
except Exception as ex:
raise ValueError(
f"`to_matrix` conversion of two-qubit gate ({gate.name}) failed. "
"Often, this can be caused by unbound parameters."
) from ex
d = TwoQubitWeylDecomposition(mat)
u = _u_from_thetavec([d.a, d.b, d.c])
retval = _nonlocal_qpd_basis_from_u(u)
for operations in unique_by_id(m[0] for m in retval.maps):
operations.insert(0, UnitaryGate(d.K2r))
operations.append(UnitaryGate(d.K1r))
for operations in unique_by_id(m[1] for m in retval.maps):
operations.insert(0, UnitaryGate(d.K2l))
operations.append(UnitaryGate(d.K1l))
return retval
raise ValueError(f"Instruction not supported: {gate.name}")
def _explicitly_supported_instructions() -> set[str]:
"""Return a set of instruction names with explicit support for automatic decomposition.
These instructions are *explicitly* supported by :func:`qpdbasis_from_instruction`.
Other instructions may be supported too, via a KAK decomposition.
Returns:
Set of gate names supported for automatic decomposition.
"""
return set(_qpdbasis_from_instruction_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 _u_from_thetavec(
theta: np.typing.NDArray[np.float64] | Sequence[float], /
) -> np.typing.NDArray[np.complex128]:
r"""Exponentiate the non-local portion of a KAK decomposition.
This implements Eq. (6) of https://arxiv.org/abs/2006.11174v2:
.. math::
\exp [ i ( \sum_\alpha^3 \theta_\alpha \, \sigma_\alpha \otimes \sigma_\alpha ) ]
=
\sum_{\alpha=0}^3 u_\alpha \, \sigma_\alpha \otimes \sigma_\alpha
where each :math:`\theta_\alpha` is assumed to be real, and
:math:`u_\alpha` is complex in general.
"""
theta = np.asarray(theta)
if theta.shape != (3,):
raise ValueError(
f"theta vector has wrong shape: {theta.shape} (1D vector of length 3 expected)"
)
# First, we note that if we choose the basis vectors II, XX, YY, and ZZ,
# then the following matrix represents one application of the summation in
# the exponential:
#
# 0 θx θy θz
# θx 0 -θz -θy
# θy -θz 0 -θx
# θz -θy -θx 0
#
# This matrix is symmetric and can be exponentiated by diagonalizing it.
# Its eigendecomposition is given by:
eigvals = np.array(
[
-np.sum(theta),
-theta[0] + theta[1] + theta[2],
-theta[1] + theta[2] + theta[0],
-theta[2] + theta[0] + theta[1],
]
)
eigvecs = np.ones([1, 1]) / 2 - np.eye(4)
# Finally, we exponentiate the eigenvalues of the matrix in diagonal form.
# We also project to the vector [1,0,0,0] on the right, since the
# multiplicative identity is given by II.
return np.transpose(eigvecs) @ (np.exp(1j * eigvals) * eigvecs[:, 0])
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 = [SXGate(), QPDMeasure(), SXdgGate()]
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_instruction("swap")
def _(unused_gate: SwapGate):
return _nonlocal_qpd_basis_from_u([(1 + 1j) / np.sqrt(8)] * 4)
@_register_qpdbasis_from_instruction("iswap")
def _(unused_gate: iSwapGate):
return _nonlocal_qpd_basis_from_u([0.5, 0.5j, 0.5j, 0.5])
@_register_qpdbasis_from_instruction("dcx")
def _(unused_gate: DCXGate):
retval = qpdbasis_from_instruction(iSwapGate())
# Modify basis according to DCXGate definition in Qiskit circuit library
# https://github.com/Qiskit/qiskit/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_instruction("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
# https://iopscience.iop.org/article/10.1088/1367-2630/abd7bc/pdf
if gate.name in ("rxx", "crx"):
pauli = XGate()
r_plus = SXGate()
# x basis measurement (and back again)
measurement_0 = [HGate(), QPDMeasure(), HGate()]
elif gate.name in ("ryy", "cry"):
pauli = YGate()
r_plus = RYGate(0.5 * np.pi)
# y basis measurement (and back again)
measurement_0 = [SXGate(), QPDMeasure(), SXdgGate()]
else:
assert gate.name in ("rzz", "crz")
pauli = ZGate()
r_plus = SGate()
# z basis measurement
measurement_0 = [QPDMeasure()]
r_minus = r_plus.inverse()
measurement_1 = measurement_0.copy()
# Specify the operations to be inserted for systems 1 and 2
maps = [
([], []), # Identity
([pauli], [pauli]),
(measurement_0, [r_plus]),
(measurement_0, [r_minus]),
([r_plus], measurement_1),
([r_minus], measurement_1),
]
theta = _theta_from_instruction(gate)
if gate.name[0] == "c":
# Following Eq. (C.4) of https://arxiv.org/abs/2205.00016v2,
# which implements controlled single qubit rotations in terms of a
# parametric two-qubit rotation.
theta = -theta / 2
# Modify `maps`
for operations in unique_by_id(m[0] for m in maps):
if operations and gate.name != "crz":
if gate.name == "cry":
operations.insert(0, SGate())
operations.append(SdgGate())
operations.insert(0, HGate())
operations.append(HGate())
rot = {"x": RXGate, "y": RYGate, "z": RZGate}[gate.name[2]](-theta)
for operations in unique_by_id(m[1] for m in maps):
operations.append(rot)
# Generate QPD coefficients for gates specified by Mitarai et al -
# https://iopscience.iop.org/article/10.1088/1367-2630/abd7bc/pd
theta_prime = -theta / 2 # theta as defined by Mitarai and Fujii
cs_theta_prime = np.cos(theta_prime) * np.sin(theta_prime)
coeffs = [
np.cos(theta_prime) ** 2,
np.sin(theta_prime) ** 2,
# Qiskit defines RZ(theta) = exp(-i*Z*theta/2), i.e., with a minus
# sign in the exponential; hence r_plus really represents
# exp(-i*Z*pi/4), and similar for RX and RY. Following Mitarai and
# Fujii Eq. (6), the correct signs are therefore given by
-cs_theta_prime,
cs_theta_prime,
-cs_theta_prime,
cs_theta_prime,
]
return QPDBasis(maps, coeffs)
@_register_qpdbasis_from_instruction("cs", "csdg")
def _(gate: CSGate | CSdgGate):
theta = np.pi / 2
rot_gate = TGate()
if gate.name == "csdg":
theta *= -1
rot_gate = rot_gate.inverse()
retval = qpdbasis_from_instruction(CRZGate(theta))
for operations in unique_by_id(m[0] for m in retval.maps):
operations.insert(0, rot_gate)
return retval
@_register_qpdbasis_from_instruction("cp")
def _(gate: CPhaseGate):
theta = _theta_from_instruction(gate)
retval = qpdbasis_from_instruction(CRZGate(theta))
for operations in unique_by_id(m[0] for m in retval.maps):
operations.insert(0, PhaseGate(theta / 2))
return retval
@_register_qpdbasis_from_instruction("csx")
def _(unused_gate: CSXGate):
retval = qpdbasis_from_instruction(CRXGate(np.pi / 2))
for operations in unique_by_id(m[0] for m in retval.maps):
operations.insert(0, TGate())
return retval
@_register_qpdbasis_from_instruction("csxdg")
def _(unused_gate: ControlledGate):
retval = qpdbasis_from_instruction(CRXGate(-np.pi / 2))
for operations in unique_by_id(m[0] for m in retval.maps):
operations.insert(0, TdgGate())
return retval
@_register_qpdbasis_from_instruction("cx", "cy", "cz", "ch")
def _(gate: CXGate | CYGate | CZGate | CHGate):
# Constructing a virtual two-qubit gate by sampling single-qubit operations - Mitarai et al
# https://iopscience.iop.org/article/10.1088/1367-2630/abd7bc/pdf
measurement_0 = [SdgGate(), QPDMeasure()]
measurement_1 = measurement_0.copy()
# Specify the operations to be inserted for systems 1 and 2
maps = [
([SdgGate()], [SdgGate()]),
([SGate()], [SGate()]),
# R-R+ simplifies to I
(measurement_0, []),
(measurement_0, [ZGate()]),
# R-R+ simplifies to I
([], measurement_1),
([ZGate()], measurement_1),
]
if gate.name != "cz":
# Modify `maps` to sandwich the target operations inside of basis rotations
for operations in unique_by_id(m[1] for m in maps):
if operations:
if gate.name in ("cx", "cy"):
operations.insert(0, HGate())
operations.append(HGate())
if gate.name == "cy":
operations.insert(0, SdgGate())
operations.append(SGate())
elif gate.name == "ch":
operations.insert(0, RYGate(-np.pi / 4))
operations.append(RYGate(np.pi / 4))
coeffs = [0.5, 0.5, 0.5, -0.5, 0.5, -0.5]
return QPDBasis(maps, coeffs)
@_register_qpdbasis_from_instruction("ecr")
def _(unused_gate: ECRGate):
retval = qpdbasis_from_instruction(CXGate())
# Modify basis according to ECRGate definition in Qiskit circuit library
# https://github.com/Qiskit/qiskit/blob/d9763523d45a747fd882a7e79cc44c02b5058916/qiskit/circuit/library/standard_gates/equivalence_library.py#L656-L663
for operations in unique_by_id(m[0] for m in retval.maps):
operations.insert(0, SGate())
operations.append(XGate())
for operations in unique_by_id(m[1] for m in retval.maps):
operations.insert(0, SXGate())
return retval
def _theta_from_instruction(gate: Gate, /) -> float:
param_gates = {"rxx", "ryy", "rzz", "crx", "cry", "crz", "cp"}
# Internal function should only be called for supported gates
assert gate.name in param_gates
# If theta is a bound ParameterExpression, convert to float, else raise error.
try:
theta = float(gate.params[0])
except TypeError as err:
raise ValueError(
f"Cannot decompose ({gate.name}) instruction with unbound parameters."
) from err
return theta
@_register_qpdbasis_from_instruction("move")
def _(unused_gate: Move):
i_measurement = [Reset()]
x_measurement = [HGate(), QPDMeasure(), Reset()]
y_measurement = [SXGate(), 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(), SXdgGate()]
prep_iminus = [Reset(), XGate(), SXdgGate()]
# 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)