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

Make TwoQubitWeylDecomposition pickle-able #7333

Merged
merged 8 commits into from
Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 7 additions & 1 deletion qiskit/quantum_info/synthesis/two_qubit_decompose.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def __init_subclass__(cls, **kwargs):
)

@staticmethod
def __new__(cls, unitary_matrix, *, fidelity=(1.0 - 1.0e-9)):
def __new__(cls, unitary_matrix, *, fidelity=(1.0 - 1.0e-9), _unpickling=False):
"""Perform the Weyl chamber decomposition, and optionally choose a specialized subclass.

The flip into the Weyl Chamber is described in B. Kraus and J. I. Cirac, Phys. Rev. A 63,
Expand All @@ -145,6 +145,9 @@ def __new__(cls, unitary_matrix, *, fidelity=(1.0 - 1.0e-9)):

The overall decomposition scheme is taken from Drury and Love, arXiv:0806.4015 [quant-ph].
"""
if _unpickling:
return super().__new__(cls)

pi = np.pi
pi2 = np.pi / 2
pi4 = np.pi / 4
Expand Down Expand Up @@ -403,6 +406,9 @@ def actual_fidelity(self, **kwargs) -> float:
trace = np.trace(Operator(circ).data.T.conj() @ self.unitary_matrix)
return trace_to_fid(trace)

def __getnewargs_ex__(self):
return (self.unitary_matrix,), {"_unpickling": True}

def __repr__(self):
"""Represent with enough precision to allow copy-paste debugging of all corner cases"""
prefix = f"{type(self).__qualname__}.from_bytes("
Expand Down
6 changes: 6 additions & 0 deletions releasenotes/notes/pickle_weyl-34e16e3aab2f7133.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---

fixes:
- |
The class `TwoQubitWeylDecomposition` and its subclasses are now pickle-safe, which is a partial fix for
`#7312 <https://github.com/Qiskit/qiskit-terra/issues/7312>`__
31 changes: 31 additions & 0 deletions test/python/quantum_info/test_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

"""Tests for quantum synthesis methods."""

import pickle
import unittest
import contextlib
import logging
Expand Down Expand Up @@ -173,6 +174,28 @@ def assertRoundTrip(self, weyl1: TwoQubitWeylDecomposition):
self.assertEqual(maxdiff, 0, msg=f"K2r matrix differs by {maxdiff}" + msg_base)
self.assertEqual(weyl1.requested_fidelity, weyl2.requested_fidelity, msg_base)

def assertRoundTripPickle(self, weyl1: TwoQubitWeylDecomposition):
"""Fail if loads(dumps(weyl1)) not equal to weyl1"""
Comment on lines +182 to +183
Copy link
Member

Choose a reason for hiding this comment

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

It may also be worth adding a very similar test that copy.deepcopy and copy.copy work correctly as well/ deepcopy presumably will because it's a full pickle/unpickle by default, but copy might do something funny; I think it basically calls __getstate__, then restores it with an immediate call to __setstate__ without recursing the pickle through the state. I would guess it does the right thing and calls __getnewargs_ex__ if defined, but I don't really know, and it might be worth a test, since Qiskit uses the copy module pretty extensively.


pkl = pickle.dumps(weyl1, protocol=max(4, pickle.DEFAULT_PROTOCOL))
weyl2 = pickle.loads(pkl)
msg_base = f"weyl1:\n{weyl1}\nweyl2:\n{repr(weyl2)}"
Copy link
Member

@jakelishman jakelishman Dec 23, 2021

Choose a reason for hiding this comment

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

I learned recently that f"{repr(x)}" can also be written as f"{x!r}" (and !s for str), which seems like a fun way to introduce a million more sigils to the strings and make them completely unreadable. When we get to Python 3.8+ only, there's an even more arcane one: f"{weyl1=}", which is equivalent to f"weyl1={weyl1!r}".

self.assertEqual(type(weyl1), type(weyl2), msg_base)
maxdiff = np.max(abs(weyl1.unitary_matrix - weyl2.unitary_matrix))
self.assertEqual(maxdiff, 0, msg=f"Unitary matrix differs by {maxdiff}\n" + msg_base)
self.assertEqual(weyl1.a, weyl2.a, msg=msg_base)
self.assertEqual(weyl1.b, weyl2.b, msg=msg_base)
self.assertEqual(weyl1.c, weyl2.c, msg=msg_base)
maxdiff = np.max(np.abs(weyl1.K1l - weyl2.K1l))
self.assertEqual(maxdiff, 0, msg=f"K1l matrix differs by {maxdiff}" + msg_base)
maxdiff = np.max(np.abs(weyl1.K1r - weyl2.K1r))
self.assertEqual(maxdiff, 0, msg=f"K1r matrix differs by {maxdiff}" + msg_base)
maxdiff = np.max(np.abs(weyl1.K2l - weyl2.K2l))
self.assertEqual(maxdiff, 0, msg=f"K2l matrix differs by {maxdiff}" + msg_base)
maxdiff = np.max(np.abs(weyl1.K2r - weyl2.K2r))
self.assertEqual(maxdiff, 0, msg=f"K2r matrix differs by {maxdiff}" + msg_base)
self.assertEqual(weyl1.requested_fidelity, weyl2.requested_fidelity, msg_base)

def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.0e-12):
"""Check TwoQubitWeylDecomposition() works for a given operator"""
# pylint: disable=invalid-name
Expand Down Expand Up @@ -207,6 +230,7 @@ def check_two_qubit_weyl_specialization(
with self.assertDebugOnly():
decomp = decomposer(target_unitary, fidelity=fidelity)
self.assertRoundTrip(decomp)
self.assertRoundTripPickle(decomp)
self.assertEqual(
np.max(np.abs(decomp.unitary_matrix - target_unitary)),
0,
Expand All @@ -228,6 +252,7 @@ def check_two_qubit_weyl_specialization(
with self.assertDebugOnly():
decomp2 = expected_specialization(target_unitary, fidelity=None) # Shouldn't raise
self.assertRoundTrip(decomp2)
self.assertRoundTripPickle(decomp2)
if expected_specialization is not TwoQubitWeylGeneral:
with self.assertRaises(QiskitError) as exc:
_ = expected_specialization(target_unitary, fidelity=1.0)
Expand Down Expand Up @@ -572,6 +597,12 @@ def test_TwoQubitWeylDecomposition_repr(self, seed=42):
weyl1 = TwoQubitWeylDecomposition(target, fidelity=0.99)
self.assertRoundTrip(weyl1)

def test_TwoQubitWeylDecomposition_pickle(self, seed=42):
"""Check that loads(dumps()) is exact round trip"""
target = random_unitary(4, seed=seed)
weyl1 = TwoQubitWeylDecomposition(target, fidelity=0.99)
self.assertRoundTripPickle(weyl1)

def test_two_qubit_weyl_decomposition_cnot(self):
"""Verify Weyl KAK decomposition for U~CNOT"""
for k1l, k1r, k2l, k2r in K1K2S:
Expand Down