-
Notifications
You must be signed in to change notification settings - Fork 34
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 cutting of general 2-qubit unitaries #302
Changes from 23 commits
b9d7aed
779525b
56a4419
a65397f
3edcc96
b114c0d
489b68d
3515e0e
1c95367
040f219
74e5a9d
f8aa065
e0a5f27
d790f1d
a9ec8b6
69a1e69
727d159
59b2691
9a7121a
4232864
05755e3
39b865e
0823787
236fad2
874d1a3
bee23a8
f43d474
8e95c4a
3bfccfc
51a9674
91ac6b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,7 @@ | |
decompose_qpd_instructions, | ||
WeightType, | ||
qpdbasis_from_gate, | ||
supported_gates, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a 2-qubit gate in Qiskit that would break our cutting workflow if we tried to cut it? I'm wondering if we can replace with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I now see your docstring update below. They have to implement |
||
explicitly_supported_gates, | ||
) | ||
from .instructions import ( | ||
BaseQPDGate, | ||
|
@@ -32,7 +32,7 @@ | |
"generate_qpd_weights", | ||
"generate_qpd_samples", | ||
"decompose_qpd_instructions", | ||
"supported_gates", | ||
"explicitly_supported_gates", | ||
"QPDBasis", | ||
"BaseQPDGate", | ||
"TwoQubitQPDGate", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,6 +62,8 @@ | |
iSwapGate, | ||
DCXGate, | ||
) | ||
from qiskit.extensions import UnitaryGate | ||
from qiskit.quantum_info.synthesis.two_qubit_decompose import TwoQubitWeylDecomposition | ||
from qiskit.utils import deprecate_func | ||
|
||
from .qpd_basis import QPDBasis | ||
|
@@ -557,45 +559,52 @@ def qpdbasis_from_gate(gate: Gate) -> QPDBasis: | |
""" | ||
Generate a QPDBasis object, given a supported operation. | ||
|
||
This method currently supports the following operations: | ||
- :class:`~qiskit.circuit.library.RXXGate` | ||
- :class:`~qiskit.circuit.library.RYYGate` | ||
- :class:`~qiskit.circuit.library.RZZGate` | ||
- :class:`~qiskit.circuit.library.CRXGate` | ||
- :class:`~qiskit.circuit.library.CRYGate` | ||
- :class:`~qiskit.circuit.library.CRZGate` | ||
- :class:`~qiskit.circuit.library.CXGate` | ||
- :class:`~qiskit.circuit.library.CYGate` | ||
- :class:`~qiskit.circuit.library.CZGate` | ||
- :class:`~qiskit.circuit.library.CHGate` | ||
- :class:`~qiskit.circuit.library.CSXGate` | ||
- :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`. | ||
The operations with explicit support can be obtained by calling | ||
:func:`explicitly_supported_gates`. | ||
|
||
Additionally, all two-qubit gates which implement the :meth:`.Gate.to_matrix` method are | ||
supported via a KAK decomposition (:class:`.TwoQubitWeylDecomposition`). | ||
|
||
Returns: | ||
The newly-instantiated :class:`QPDBasis` object | ||
|
||
Raises: | ||
ValueError: Gate not supported. | ||
ValueError: Cannot decompose gate with unbound parameters. | ||
ValueError: ``to_matrix`` conversion of two-qubit gate failed. | ||
""" | ||
try: | ||
f = _qpdbasis_from_gate_funcs[gate.name] | ||
except KeyError: | ||
raise ValueError(f"Gate not supported: {gate.name}") from None | ||
pass | ||
else: | ||
return f(gate) | ||
|
||
if isinstance(gate, Gate) and gate.num_qubits == 2: | ||
caleb-johnson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
try: | ||
mat = gate.to_matrix() | ||
except Exception as ex: | ||
raise ValueError("`to_matrix` conversion of two-qubit gate failed") 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"Gate not supported: {gate.name}") from None | ||
|
||
def supported_gates() -> set[str]: | ||
|
||
def explicitly_supported_gates() -> set[str]: | ||
""" | ||
Return a set of gate names supported for automatic decomposition. | ||
Return a set of instruction names with explicit support for automatic decomposition. | ||
|
||
These instructions are *explicitly* supported by :func:`qpdbasis_from_gate`. | ||
Other instructions may be supported too, via a KAK decomposition. | ||
|
||
Returns: | ||
Set of gate names supported for automatic decomposition. | ||
|
@@ -619,6 +628,54 @@ def _copy_unique_sublists(lsts: tuple[list, ...], /) -> tuple[list, ...]: | |
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great writeup here, thanks |
||
|
||
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: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
--- | ||
upgrade: | ||
- | | ||
Addition of :func:`~circuit_knitting.cutting.qpd.supported_gates` function, which returns the names of all gates which may be automatically decomposed using :func:`~circuit_knitting.cutting.qpd.qpdbasis_from_gate`. | ||
Addition of :func:`~circuit_knitting.cutting.qpd.explicitly_supported_gates` function, which returns the names of all gates which are explicitly supported by :func:`~circuit_knitting.cutting.qpd.qpdbasis_from_gate`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to suggest
optimally_supported_gates
, but I can't convince myself I like it more. I think they are all optimal, at least?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, they are all optimal -- even the ones for which we do not have explicit support.
Another option could be to get rid of this function -- or at least to make it private. The reason it was introduced in #277 was to support #278, but now that all two-qubit gates can be cut, it's no longer necessary. It might be nice to have something like this if we one day support some (but not all) 3-qubit gates (#258), which could be a case for keeping it but prefixing it with an underscore. This way, we'd at least have a use case in mind (again) before committing to support it.
By the way, all essential tests (i.e., all but a few specific ones) work if we take this PR and remove all the explicit gate support, i.e., if we apply the further patch:
so explicit support doesn't mean a ton anyway.
With #174, there will be one additional explicitly supported instruction: the
Move
instruction, which is not aGate
because it is not unitary. So it might be nice to have a function like the current one, somewhere. I'm leaning toward making the whole function private for now and removing it from the release notes until we better understand how we expect it to be used.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The more I thought about it, the more I felt like the most reasonable thing to do is to remove this function from the public API for now, hence my change in 8e95c4a.