|
10 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 or in the LICENSE file in the root pyGSTi directory. |
11 | 11 | #*************************************************************************************************** |
12 | 12 |
|
| 13 | +from __future__ import annotations |
| 14 | + |
13 | 15 | from collections import OrderedDict |
14 | 16 | import copy as _copy |
15 | 17 |
|
16 | 18 | import numpy as _np |
17 | 19 |
|
18 | 20 | from pygsti.baseobjs.nicelyserializable import NicelySerializable as _NicelySerializable |
| 21 | +from pygsti.pgtypes import SpaceT |
19 | 22 | from pygsti.tools import listtools as _lt |
20 | 23 | from pygsti.tools import slicetools as _slct |
| 24 | +from pygsti.tools import matrixtools as _mt |
| 25 | + |
| 26 | +from typing import Optional |
21 | 27 |
|
22 | 28 |
|
23 | 29 | class ModelChild(object): |
@@ -254,6 +260,10 @@ def parent(self): |
254 | 260 | ------- |
255 | 261 | Model |
256 | 262 | """ |
| 263 | + if '_parent' not in self.__dict__: |
| 264 | + # This can be absent because of how serialization works. |
| 265 | + # It's set during deserialization in self.relink_parent(). |
| 266 | + self._parent = None |
257 | 267 | return self._parent |
258 | 268 |
|
259 | 269 | @parent.setter |
@@ -308,11 +318,13 @@ def relink_parent(self, parent): |
308 | 318 | None |
309 | 319 | """ |
310 | 320 | 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 |
| 321 | + subm.relink_parent(parent) |
| 322 | + if '_parent' in self.__dict__: |
| 323 | + # This codepath needed to resolve GitHub issue 651. |
| 324 | + if self._parent is parent: |
| 325 | + return # OK to relink multiple times |
| 326 | + assert(self._parent is None), "Cannot relink parent: current parent is not None!" |
| 327 | + self._parent = parent |
316 | 328 |
|
317 | 329 | def unlink_parent(self, force=False): |
318 | 330 | """ |
@@ -793,6 +805,77 @@ def copy(self, parent=None, memo=None): |
793 | 805 | memo[id(self.parent)] = None # so deepcopy uses None instead of copying parent |
794 | 806 | return self._copy_gpindices(_copy.deepcopy(self, memo), parent, memo) |
795 | 807 |
|
| 808 | + def to_dense(self) -> _np.ndarray: |
| 809 | + raise NotImplementedError('Derived classes must implement .to_dense().') |
| 810 | + |
| 811 | + def _to_transformed_dense(self, T_domain: _mt.OperatorLike, T_codomain: _mt.OperatorLike, on_space: SpaceT='minimal') -> _np.ndarray: |
| 812 | + """ |
| 813 | + Return an array, XT, obtained by suitably transforming X = self.to_dense(on_space). |
| 814 | +
|
| 815 | + The basic nature of the transformation X --> XT depends on the category of `self`, |
| 816 | + as determined by its domain and codomain. |
| 817 | +
|
| 818 | + | abstract category | domain | codomain | |
| 819 | + | ----------------- | ------------ | ------------ | |
| 820 | + | vector | field | vector space | |
| 821 | + | functional | vector space | field | |
| 822 | + | operator | vector space | vector space | |
| 823 | +
|
| 824 | + To state the specific transformation X --> XT, let op(X) denote the operator |
| 825 | + representation of X obtained by (1) interpreting fields as 1-dimensional vector |
| 826 | + spaces, and (2) having linear operators act on vectors by left-multiplication. |
| 827 | +
|
| 828 | + The returned array, XT, is defined through its op(XT) representation: |
| 829 | +
|
| 830 | + | abstract category | op(XT) representation of XT | |
| 831 | + | ----------------- | ----------------------------- | |
| 832 | + | vector | T_codomain @ op(X) | |
| 833 | + | functional | op(X) @ T_domain | |
| 834 | + | operator | T_codomain @ op(X) @ T_domain | |
| 835 | +
|
| 836 | + Note that T_domain is ignored for abstract vectors (i.e., state prep), and T_codomain |
| 837 | + is ignored for abstract functionals (i.e., POVM effects). |
| 838 | + """ |
| 839 | + raise NotImplementedError() |
| 840 | + |
| 841 | + def residuals(self, other: ModelMember, |
| 842 | + transform: Optional[_mt.OperatorLike]=None, inv_transform: Optional[_mt.OperatorLike]=None |
| 843 | + ) -> _np.ndarray: |
| 844 | + # This implementation was introduced as part of a heavy refactor, but it preserves all intended |
| 845 | + # semantics of the old implementation. |
| 846 | + T_domain = _mt.to_operatorlike(transform) |
| 847 | + T_codomain = _mt.to_operatorlike(inv_transform) |
| 848 | + # ^ to_operatorlike casts None to IdentityOperator |
| 849 | + X = self._to_transformed_dense(T_domain, T_codomain) |
| 850 | + if isinstance(inv_transform, _mt.IdentityOperator): |
| 851 | + # Passing inv_transform as an IdentityOperator (rather than casting from None) |
| 852 | + # is a flag. It indicates that we want to apply `transform` to `other` as well. |
| 853 | + # |
| 854 | + # (Yes, this sort of flag interpretation is bad design. No, I don't want to |
| 855 | + # spend the time on a good design.) |
| 856 | + Y = other._to_transformed_dense(T_domain, inv_transform) |
| 857 | + else: |
| 858 | + Y = other.to_dense() |
| 859 | + return (X - Y).ravel() |
| 860 | + |
| 861 | + def frobeniusdist_squared(self, other: ModelMember, |
| 862 | + transform: Optional[_mt.OperatorLike]=None, inv_transform: Optional[_mt.OperatorLike]=None |
| 863 | + ) -> _np.floating: |
| 864 | + """ |
| 865 | + Return the squared Frobenius norm of the difference between `self` and `other`, |
| 866 | + possibly after transformation by `transform` and/or `inv_transform`. |
| 867 | + """ |
| 868 | + return _np.linalg.norm(self.residuals(other, transform, inv_transform))**2 |
| 869 | + |
| 870 | + def frobeniusdist(self, other: ModelMember, |
| 871 | + transform: Optional[_mt.OperatorLike]=None, inv_transform: Optional[_mt.OperatorLike]=None |
| 872 | + ) -> _np.floating: |
| 873 | + """ |
| 874 | + Return the Frobenius norm of the difference between `self` and `other`, |
| 875 | + possibly after transformation by `transform` and/or `inv_transform`. |
| 876 | + """ |
| 877 | + return _np.linalg.norm(self.residuals(other, transform, inv_transform)) |
| 878 | + |
796 | 879 | def _is_similar(self, other, rtol, atol): |
797 | 880 | """ Returns True if `other` model member (which it guaranteed to be the same type as self) has |
798 | 881 | the same local structure, i.e., not considering parameter values or submembers """ |
@@ -857,7 +940,16 @@ def is_equivalent(self, other, rtol=1e-5, atol=1e-8): |
857 | 940 | """ |
858 | 941 | if not self.is_similar(other): return False |
859 | 942 |
|
860 | | - if not _np.allclose(self.to_vector(), other.to_vector(), rtol=rtol, atol=atol): |
| 943 | + try: |
| 944 | + v1 = self.to_vector() |
| 945 | + v2 = other.to_vector() |
| 946 | + except RuntimeError as e: |
| 947 | + if 'to_vector() should never be called' not in str(e): |
| 948 | + raise e |
| 949 | + assert type(self) == type(other) |
| 950 | + v1 = self.to_dense() |
| 951 | + v2 = other.to_dense() |
| 952 | + if not _np.allclose(v1, v2, rtol=rtol, atol=atol): |
861 | 953 | return False |
862 | 954 |
|
863 | 955 | # Recursive check on submembers |
|
0 commit comments