Skip to content

Commit 117fb15

Browse files
authored
Leakage modeling: bugfixes for wildcard error and gauge optimization (#671)
This PR is split off from #664. It fixes how wildcard error is computed in leakage modeling. When merged it will resolve #652. It also includes changes to the default leakage-aware gauge optimization suite. The latter changes are needed to prevent gauge optimization from trying to spuriously "spread out" relational errors across available gates. This PR adds a meaningful correctness test for leakage GST modeling. To reduce the time of the test I used a huge number of shots per circuit in the simulated dataset. This led to errors with "min_prob_clip" constants in objective function evaluation. In order to resolve those errors I replaced hard-coded instances of that constant's default value (1e-4) to values of a module-wide constant; I made similar changes for "radius" constants in objective functions. Finally, this PR adds implementations of frobeniusdist, frobeniusdist_squared, and residual to ModelMember, and removes the implementation of these functions from all child classes. Removing the child class implementation become possible because the ModelMember.residual implementation relies on a new method, ModelMember._to_transformed_dense(...), that child classes must implement. The semantics of _to_transformed_dense are documented in ModelMember. I provided implementations for base classes for POVM effects, states, and linear operators. (This PR is a superset of the since-closed PR #670, which started from develop instead of bugfix.)
1 parent b5feea1 commit 117fb15

File tree

13 files changed

+650
-475
lines changed

13 files changed

+650
-475
lines changed

jupyter_notebooks/Examples/Leakage-automagic.ipynb

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
"metadata": {},
77
"outputs": [],
88
"source": [
9-
"from pygsti.modelpacks import smq1Q_XY, smq1Q_ZN\n",
9+
"from pygsti.modelpacks import smq1Q_XYI as mp\n",
1010
"from pygsti.tools.leakage import leaky_qubit_model_from_pspec, construct_leakage_report\n",
1111
"from pygsti.data import simulate_data\n",
12-
"from pygsti.protocols import StandardGST, ProtocolData"
12+
"from pygsti.protocols import StandardGST, ProtocolData\n",
13+
"import numpy as np\n",
14+
"import scipy.linalg as la"
1315
]
1416
},
1517
{
@@ -27,14 +29,51 @@
2729
"metadata": {},
2830
"outputs": [],
2931
"source": [
30-
"mp = smq1Q_XY\n",
31-
"ed = mp.create_gst_experiment_design(max_max_length=32)\n",
32+
"def with_leaky_gate(m, gate_label, strength):\n",
33+
" rng = np.random.default_rng(0)\n",
34+
" v = np.concatenate([[0.0], rng.standard_normal(size=(2,))])\n",
35+
" v /= la.norm(v)\n",
36+
" H = v.reshape((-1, 1)) @ v.reshape((1, -1))\n",
37+
" H *= strength\n",
38+
" U = la.expm(1j*H)\n",
39+
" m_copy = m.copy()\n",
40+
" G_ideal = m_copy.operations[gate_label]\n",
41+
" from pygsti.modelmembers.operations import ComposedOp, StaticUnitaryOp\n",
42+
" m_copy.operations[gate_label] = ComposedOp([G_ideal, StaticUnitaryOp(U, basis=m.basis)])\n",
43+
" return m_copy, v\n"
44+
]
45+
},
46+
{
47+
"cell_type": "code",
48+
"execution_count": null,
49+
"metadata": {},
50+
"outputs": [],
51+
"source": [
52+
"ed = mp.create_gst_experiment_design(max_max_length=8)\n",
53+
"# ^ The default max length is small so we don't have to wait as long \n",
54+
"# for the GST fit (just for purposes of this notebook).\n",
3255
"tm3 = leaky_qubit_model_from_pspec(mp.processor_spec(), mx_basis='l2p1')\n",
33-
"# ^ We could use basis = 'gm' instead of 'l2p1'. We prefer 'l2p1'\n",
34-
"# because it makes process matrices easier to interpret in leakage\n",
35-
"# modeling.\n",
36-
"ds = simulate_data(tm3, ed.all_circuits_needing_data, num_samples=1000, seed=1997)\n",
37-
"gst = StandardGST( modes=('CPTPLND',), target_model=tm3, verbosity=2)\n",
56+
"# ^ Target model. \"Leaky\" is a bit of a misnomer here. The returned model\n",
57+
"# is simply a qutrit lift of the qubit model; leakage erorrs in the\n",
58+
"# qubit model can manifest as CPTP Markovian errors in the qutrit model.\n",
59+
"dgm3, leaking_state = with_leaky_gate(tm3, ('Gxpi2', 0), strength=0.125)\n",
60+
"# ^ Data generating model. \n",
61+
"num_samples = 100_000\n",
62+
"# ^ The number of samples is large to compensate for short circuit length.\n",
63+
"# Feel free to change the number of samples to something more \"realistic\"\n",
64+
"# if you'd like.\n",
65+
"if num_samples > 10_000:\n",
66+
" from pygsti.objectivefns import objectivefns\n",
67+
" objectivefns.DEFAULT_MIN_PROB_CLIP = objectivefns.DEFAULT_RADIUS = 1e-12\n",
68+
" # ^ There are numerical thresholding rules in objective function evaluation\n",
69+
" # that lead to errors when the number of samples is extremely large.\n",
70+
" # The lines above change those thresholding rules to be appropriate in\n",
71+
" # the unusual setting that is this notebook.\n",
72+
"ds = simulate_data(dgm3, ed.all_circuits_needing_data, num_samples=num_samples, seed=1997)\n",
73+
"gst = StandardGST(\n",
74+
" modes=('CPTPLND',), target_model=tm3, verbosity=2,\n",
75+
" badfit_options={'actions': ['wildcard1d'], 'threshold': 0.0}\n",
76+
")\n",
3877
"pd = ProtocolData(ed, ds)\n",
3978
"res = gst.run(pd)"
4079
]

pygsti/modelmembers/modelmember.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,20 @@
1010
# http://www.apache.org/licenses/LICENSE-2.0 or in the LICENSE file in the root pyGSTi directory.
1111
#***************************************************************************************************
1212

13+
from __future__ import annotations
14+
1315
from collections import OrderedDict
1416
import copy as _copy
1517

1618
import numpy as _np
1719

1820
from pygsti.baseobjs.nicelyserializable import NicelySerializable as _NicelySerializable
21+
from pygsti.pgtypes import SpaceT
1922
from pygsti.tools import listtools as _lt
2023
from pygsti.tools import slicetools as _slct
24+
from pygsti.tools import matrixtools as _mt
25+
26+
from typing import Optional
2127

2228

2329
class ModelChild(object):
@@ -802,6 +808,74 @@ def copy(self, parent=None, memo=None):
802808
def to_dense(self) -> _np.ndarray:
803809
raise NotImplementedError('Derived classes must implement .to_dense().')
804810

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+
805879
def _is_similar(self, other, rtol, atol):
806880
""" Returns True if `other` model member (which it guaranteed to be the same type as self) has
807881
the same local structure, i.e., not considering parameter values or submembers """

pygsti/modelmembers/operations/linearop.py

Lines changed: 11 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@
1010
# http://www.apache.org/licenses/LICENSE-2.0 or in the LICENSE file in the root pyGSTi directory.
1111
#***************************************************************************************************
1212

13+
from __future__ import annotations
14+
1315
import numpy as _np
1416

1517
from pygsti.baseobjs.opcalc import bulk_eval_compact_polynomials_complex as _bulk_eval_compact_polynomials_complex
1618
from pygsti.modelmembers import modelmember as _modelmember
1719
from pygsti.tools import optools as _ot
20+
from pygsti.tools import matrixtools as _mt
1821
from pygsti import SpaceT
1922

20-
from typing import Any
21-
2223
#Note on initialization sequence of Operations within a Model:
2324
# 1) a Model is constructed (empty)
2425
# 2) a LinearOperator is constructed - apart from a Model if it's locally parameterized,
@@ -156,6 +157,14 @@ def to_dense(self, on_space: SpaceT='minimal'):
156157
"""
157158
raise NotImplementedError("to_dense(...) not implemented for %s objects!" % self.__class__.__name__)
158159

160+
def _to_transformed_dense(self, T_domain: _mt.OperatorLike, T_codomain: _mt.OperatorLike, on_space: SpaceT='minimal') -> _np.ndarray:
161+
"""
162+
Return an array representation of the linear operator obtained by composing T_domain,
163+
self.to_dense(), and T_codomain --- in that order.
164+
"""
165+
out = T_codomain @ self.to_dense(on_space=on_space) @ T_domain
166+
return out
167+
159168
def acton(self, state, on_space='minimal'):
160169
"""
161170
Act with this operator upon `state`
@@ -391,96 +400,6 @@ def taylor_order_terms_above_mag(self, order, max_polynomial_vars, min_term_mag)
391400

392401
return [t for t in terms_at_order if t.magnitude >= min_term_mag]
393402

394-
def frobeniusdist_squared(self, other_op, transform=None, inv_transform=None) -> _np.floating[Any]:
395-
"""
396-
Return the squared frobenius difference between this operation and `other_op`
397-
398-
Optionally transforms this operation first using matrices
399-
`transform` and `inv_transform`. Specifically, this operation gets
400-
transformed as: `O => inv_transform * O * transform` before comparison with
401-
`other_op`.
402-
403-
Parameters
404-
----------
405-
other_op : DenseOperator
406-
The other operation.
407-
408-
transform : numpy.ndarray, optional
409-
Transformation matrix.
410-
411-
inv_transform : numpy.ndarray, optional
412-
Inverse of `transform`.
413-
414-
Returns
415-
-------
416-
float
417-
"""
418-
self_mx = self.to_dense("minimal")
419-
if transform is not None:
420-
self_mx = self_mx @ transform
421-
if inv_transform is not None:
422-
self_mx = inv_transform @ self_mx
423-
return _ot.frobeniusdist_squared(self_mx, other_op.to_dense("minimal"))
424-
425-
426-
def frobeniusdist(self, other_op, transform=None, inv_transform=None):
427-
"""
428-
Return the frobenius distance between this operation and `other_op`.
429-
430-
Optionally transforms this operation first using matrices
431-
`transform` and `inv_transform`. Specifically, this operation gets
432-
transformed as: `O => inv_transform * O * transform` before comparison with
433-
`other_op`.
434-
435-
Parameters
436-
----------
437-
other_op : DenseOperator
438-
The other operation.
439-
440-
transform : numpy.ndarray, optional
441-
Transformation matrix.
442-
443-
inv_transform : numpy.ndarray, optional
444-
Inverse of `transform`.
445-
446-
Returns
447-
-------
448-
float
449-
"""
450-
return _np.sqrt(self.frobeniusdist_squared(other_op, transform, inv_transform))
451-
452-
def residuals(self, other_op, transform=None, inv_transform=None):
453-
"""
454-
The per-element difference between this `DenseOperator` and `other_op`.
455-
456-
Optionally, tansforming this operation first as
457-
`O => inv_transform * O * transform`.
458-
459-
Parameters
460-
----------
461-
other_op : DenseOperator
462-
The operation to compare against.
463-
464-
transform : numpy.ndarray, optional
465-
Transformation matrix.
466-
467-
inv_transform : numpy.ndarray, optional
468-
Inverse of `transform`.
469-
470-
Returns
471-
-------
472-
numpy.ndarray
473-
A 1D-array of size equal to that of the flattened operation matrix.
474-
"""
475-
dense_self = self.to_dense("minimal")
476-
if transform is not None:
477-
assert inv_transform is not None
478-
dense_self = inv_transform @ (dense_self @ transform)
479-
else:
480-
assert inv_transform is None
481-
return (dense_self - other_op.to_dense("minimal")).ravel()
482-
483-
484403
def jtracedist(self, other_op, transform=None, inv_transform=None):
485404
"""
486405
Return the Jamiolkowski trace distance between this operation and `other_op`.

pygsti/modelmembers/povms/effect.py

Lines changed: 14 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@
99
# in compliance with the License. You may obtain a copy of the License at
1010
# http://www.apache.org/licenses/LICENSE-2.0 or in the LICENSE file in the root pyGSTi directory.
1111
#***************************************************************************************************
12+
from __future__ import annotations
1213

1314
import numpy as _np
1415

16+
from pygsti.pgtypes import SpaceT
1517
from pygsti.modelmembers import modelmember as _modelmember
1618
from pygsti.tools import optools as _ot
19+
from pygsti.tools import matrixtools as _mt
1720
from pygsti.baseobjs.opcalc import bulk_eval_compact_polynomials_complex as _bulk_eval_compact_polynomials_complex
1821

19-
from typing import Any
20-
2122

2223
class POVMEffect(_modelmember.ModelMember):
2324
"""
@@ -119,59 +120,19 @@ def set_time(self, t):
119120

120121
## PUT term calc methods here if appropriate...
121122

122-
def frobeniusdist_squared(self, other_spam_vec, transform=None, inv_transform=None) -> _np.floating[Any]:
123-
"""
124-
Return the squared frobenius difference between this effect and `other_spam_vec`.
125-
126-
Optionally transforms this vector first using `transform`.
127-
128-
Parameters
129-
----------
130-
other_spam_vec : POVMEffect
131-
The other spam vector
132-
133-
transform : numpy.ndarray, optional
134-
Transformation matrix.
135-
136-
inv_transform : numpy.ndarray, optional
137-
Ignored. (We keep this as a positional argument for consistency with
138-
the frobeniusdist_squared method of pyGSTi's LinearOperator objects.)
139-
140-
Returns
141-
-------
142-
float
143-
"""
144-
vec = self.to_dense()
145-
if transform is not None:
146-
vec = transform.T @ vec
147-
return _ot.frobeniusdist_squared(vec, other_spam_vec.to_dense())
148-
149-
def residuals(self, other_spam_vec, transform=None, inv_transform=None):
123+
def _to_transformed_dense(self, T_domain: _mt.OperatorLike, T_codomain: _mt.OperatorLike, on_space: SpaceT='minimal') -> _np.ndarray:
150124
"""
151-
Return a vector of residuals between this spam vector and `other_spam_vec`.
152-
153-
Optionally transforms this vector first using `transform` and
154-
`inv_transform`.
155-
156-
Parameters
157-
----------
158-
other_spam_vec : POVMEffect
159-
The other spam vector
160-
161-
transform : numpy.ndarray, optional
162-
Transformation matrix.
163-
164-
inv_transform : numpy.ndarray, optional
165-
Inverse of `tranform`.
166-
167-
Returns
168-
-------
169-
float
125+
Return an array representation of the linear operator obtained by composing T_domain
126+
and self.to_dense(). The representation interprets POVM effects as linear functionals
127+
on density matrices. It allows for --- but does not strictly require --- the convention
128+
that POVM effects are represented as column-vector superkets.
129+
130+
T_codomain (ignored) is only here for consistency across the ModelMember API.
170131
"""
171-
vec = self.to_dense()
172-
if transform is not None:
173-
vec = transform.T @ vec
174-
return (vec - other_spam_vec.to_dense()).ravel()
132+
X = self.to_dense(on_space=on_space) # type: ignore
133+
assert X.ndim == 1 or X.shape[1] == 1
134+
out = T_domain.T @ X
135+
return out
175136

176137
def transform_inplace(self, s):
177138
"""

0 commit comments

Comments
 (0)