Skip to content

Commit d928f8a

Browse files
authored
Safeguards for ModelMember.parent and ModelMember.is_equivalent (#657)
This PR resolves #600 and #651.
1 parent f58a328 commit d928f8a

File tree

2 files changed

+44
-6
lines changed

2 files changed

+44
-6
lines changed

pygsti/modelmembers/modelmember.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ def parent(self):
254254
-------
255255
Model
256256
"""
257+
if '_parent' not in self.__dict__:
258+
# This can be absent because of how serialization works.
259+
# It's set during deserialization in self.relink_parent().
260+
self._parent = None
257261
return self._parent
258262

259263
@parent.setter
@@ -308,11 +312,13 @@ def relink_parent(self, parent):
308312
None
309313
"""
310314
for subm in self.submembers():
311-
subm.relink_parent(parent)
312-
313-
if self._parent is parent: return # OK to relink multiple times
314-
assert(self._parent is None), "Cannot relink parent: parent is not None!"
315-
self._parent = parent # assume no dependent objects
315+
subm.relink_parent(parent)
316+
if '_parent' in self.__dict__:
317+
# This codepath needed to resolve GitHub issue 651.
318+
if self._parent is parent:
319+
return # OK to relink multiple times
320+
assert(self._parent is None), "Cannot relink parent: current parent is not None!"
321+
self._parent = parent
316322

317323
def unlink_parent(self, force=False):
318324
"""
@@ -793,6 +799,9 @@ def copy(self, parent=None, memo=None):
793799
memo[id(self.parent)] = None # so deepcopy uses None instead of copying parent
794800
return self._copy_gpindices(_copy.deepcopy(self, memo), parent, memo)
795801

802+
def to_dense(self) -> _np.ndarray:
803+
raise NotImplementedError('Derived classes must implement .to_dense().')
804+
796805
def _is_similar(self, other, rtol, atol):
797806
""" Returns True if `other` model member (which it guaranteed to be the same type as self) has
798807
the same local structure, i.e., not considering parameter values or submembers """
@@ -857,7 +866,16 @@ def is_equivalent(self, other, rtol=1e-5, atol=1e-8):
857866
"""
858867
if not self.is_similar(other): return False
859868

860-
if not _np.allclose(self.to_vector(), other.to_vector(), rtol=rtol, atol=atol):
869+
try:
870+
v1 = self.to_vector()
871+
v2 = other.to_vector()
872+
except RuntimeError as e:
873+
if 'to_vector() should never be called' not in str(e):
874+
raise e
875+
assert type(self) == type(other)
876+
v1 = self.to_dense()
877+
v2 = other.to_dense()
878+
if not _np.allclose(v1, v2, rtol=rtol, atol=atol):
861879
return False
862880

863881
# Recursive check on submembers

test/unit/modelmembers/test_operation.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,26 @@ def test_convert(self):
394394
conv = op.convert(self.gate, "full TP", "gm")
395395
# TODO assert correctness
396396

397+
def test_deepcopy(self):
398+
# Issue #651 showed that an infinite recursion error is possible
399+
# when deep-copying a FullTPOp whose ._parent field is not None.
400+
# This test triggers that code path. It should pass after applying
401+
# the proposed fix in #651.
402+
#
403+
# In order to use m1.is_equivalent(m2) in the correctness check
404+
# we had to resolve GitHub issue #600.
405+
import copy
406+
from pygsti.modelpacks import smq1Q_XY
407+
m1 = smq1Q_XY.target_model('full TP')
408+
o1 = m1.operations[('Gxpi2',0)]
409+
o2 = copy.deepcopy(o1)
410+
m2 = o2.parent
411+
assert id(o1) != id(o2)
412+
assert id(m1) != id(m2)
413+
res = m1.is_equivalent(m2, rtol=0, atol=0)
414+
assert res
415+
return
416+
397417
def test_first_row_read_only(self):
398418
# check that first row is read-only
399419
e1 = self.gate[0, 0]

0 commit comments

Comments
 (0)