Skip to content

Commit b5feea1

Browse files
authored
Ensure that gauge optimization incorporates instruments (#672)
This resolves #644 by making changes to the _create_objective_fn helper called by gaugeopt_to_target. The fix was easier than I expected for two reasons. First, gauge optimization w.r.t. the Frobenius norm already incorporated instruments by way of ExplicitOpModelCalc.frobeniusdist and ExplicitOpModelCalc.residuals. Second, gauge optimization w.r.t. trace distance or infidelity can incorporate instruments just by working with operation dicts from ``target_model._excalc()`` and ``mdl._excalc()``, rather than the operation dicts in ``target_model`` and ``mdl``. This PR's solution is somewhat suboptimal in that it increases reliance on ExplicitOpModelCalc, and a private method for constructing an ExplicitOpModelCalc at that. In a subsequent PR on general leakage modeling I'll revise _create_objective_fn to replace the current solution with something more elegant.
1 parent 839884b commit b5feea1

File tree

5 files changed

+211
-141
lines changed

5 files changed

+211
-141
lines changed

pygsti/algorithms/gaugeopt.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
TrivialGaugeGroupElement as _TrivialGaugeGroupElement,
2525
GaugeGroupElement as _GaugeGroupElement
2626
)
27+
from pygsti.models import ExplicitOpModel
28+
from pygsti.modelmembers.operations import LinearOperator as _LinearOperator
2729

2830
from typing import Callable, Union, Optional
2931

@@ -360,8 +362,10 @@ def _call_jacobian_fn(gauge_group_el_vec):
360362

361363
GGElJacobian = Union[None, Callable[[_GaugeGroupElement], _np.ndarray]]
362364

365+
LabelLike = Union[str, _baseobjs.Label]
363366

364-
def _create_objective_fn(model, target_model, item_weights: Optional[dict[str,float]]=None,
367+
368+
def _create_objective_fn(model, target_model, item_weights: Optional[dict[LabelLike, float]]=None,
365369
cptp_penalty_factor: float=0.0, spam_penalty_factor: float=0.0,
366370
gates_metric="frobenius", spam_metric="frobenius",
367371
method=None, comm=None, check_jac=False, n_leak=0) -> tuple[GGElObjective, GGElJacobian]:
@@ -625,8 +629,14 @@ def _mock_objective_fn(v):
625629
# ^ It would be equivalent to set this to a pair of IdentityOperator objects.
626630

627631
def _objective_fn(gauge_group_el, oob_check):
628-
mdl = _transform_with_oob_check(model, gauge_group_el, oob_check)
629-
ret = 0
632+
mdl : ExplicitOpModel = _transform_with_oob_check(model, gauge_group_el, oob_check)
633+
mdl_ops : dict[_baseobjs.Label, _LinearOperator] = mdl._excalc().operations
634+
tgt_ops : dict[_baseobjs.Label, _LinearOperator] = dict()
635+
if target_model is not None:
636+
tgt_ops.update(target_model._excalc().operations)
637+
# ^ Use these dicts instead of mdl.operations and target_model.operations,
638+
# since these dicts are updated to include instruments.
639+
ret = 0.0
630640

631641
if cptp_penalty_factor > 0:
632642
mdl.basis = mxBasis # set basis for jamiolkowski iso
@@ -645,30 +655,27 @@ def _objective_fn(gauge_group_el, oob_check):
645655
if spam_metric == gates_metric:
646656
val = mdl.frobeniusdist(target_model, transform_mx_arg, item_weights)
647657
else:
648-
wts = item_weights.copy()
649-
wts['spam'] = 0.0
650-
for k in wts:
651-
if k in mdl.preps or k in mdl.povms:
652-
wts[k] = 0.0
653-
val = mdl.frobeniusdist(target_model, transform_mx_arg, wts, n_leak)
658+
non_gates = {'spam'}.union(set(mdl.preps)).union(set(mdl.povms))
659+
temp_wts = {k: (0.0 if k in non_gates else v) for (k, v) in item_weights.items()}
660+
val = mdl.frobeniusdist(target_model, transform_mx_arg, temp_wts)
654661
if "squared" in gates_metric:
655662
val = val ** 2
656663
ret += val
657664

658665
elif gates_metric == "fidelity":
659666
# If n_leak==0, then subspace_entanglement_fidelity is just entanglement_fidelity
660-
for opLbl in mdl.operations:
667+
for opLbl in mdl_ops:
661668
wt = item_weights.get(opLbl, opWeight)
662-
top = target_model.operations[opLbl].to_dense()
663-
mop = mdl.operations[opLbl].to_dense()
669+
top = tgt_ops[opLbl].to_dense()
670+
mop = mdl_ops[opLbl].to_dense()
664671
ret += wt * (1.0 - _tools.subspace_entanglement_fidelity(top, mop, mxBasis, n_leak))**2
665672

666673
elif gates_metric == "tracedist":
667674
# If n_leak==0, then subspace_jtracedist is just jtracedist.
668-
for opLbl in mdl.operations:
675+
for opLbl in mdl_ops:
669676
wt = item_weights.get(opLbl, opWeight)
670-
top = target_model.operations[opLbl].to_dense()
671-
mop = mdl.operations[opLbl].to_dense()
677+
top = tgt_ops[opLbl].to_dense()
678+
mop = mdl_ops[opLbl].to_dense()
672679
ret += wt * _tools.subspace_jtracedist(top, mop, mxBasis, n_leak)
673680

674681
else:
@@ -680,11 +687,9 @@ def _objective_fn(gauge_group_el, oob_check):
680687

681688
if "frobenius" in spam_metric:
682689
# SPAM and gates can have different choices for squared vs non-squared.
683-
wts = item_weights.copy(); wts['gates'] = 0.0
684-
for k in wts:
685-
if k in mdl.operations or k in mdl.instruments:
686-
wts[k] = 0.0
687-
val = mdl.frobeniusdist(target_model, transform_mx_arg, wts)
690+
non_spam = {'gates'}.union(set(mdl_ops)) # use mdl_ops, not mdl.operations!
691+
temp_wts = {k: (0.0 if k in non_spam else v) for (k, v) in item_weights.items()}
692+
val = mdl.frobeniusdist(target_model, transform_mx_arg, temp_wts)
688693
if "squared" in spam_metric:
689694
val = val ** 2
690695
ret += val

pygsti/modelmembers/operations/linearop.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -506,9 +506,8 @@ def jtracedist(self, other_op, transform=None, inv_transform=None):
506506
if transform is None and inv_transform is None:
507507
return _ot.jtracedist(self.to_dense("minimal"), other_op.to_dense("minimal"))
508508
else:
509-
return _ot.jtracedist(_np.dot(
510-
inv_transform, _np.dot(self.to_dense("minimal"), transform)),
511-
other_op.to_dense("minimal"))
509+
arg = inv_transform @ self.to_dense("minimal") @ transform
510+
return _ot.jtracedist(arg, other_op.to_dense("minimal"))
512511

513512
def diamonddist(self, other_op, transform=None, inv_transform=None):
514513
"""
@@ -535,9 +534,8 @@ def diamonddist(self, other_op, transform=None, inv_transform=None):
535534
if transform is None and inv_transform is None:
536535
return _ot.diamonddist(self.to_dense("minimal"), other_op.to_dense("minimal"))
537536
else:
538-
return _ot.diamonddist(_np.dot(
539-
inv_transform, _np.dot(self.to_dense("minimal"), transform)),
540-
other_op.to_dense("minimal"))
537+
arg = inv_transform @ self.to_dense("minimal") @ transform
538+
return _ot.diamonddist(arg, other_op.to_dense("minimal"))
541539

542540
def transform_inplace(self, s):
543541
"""
@@ -563,7 +561,7 @@ def transform_inplace(self, s):
563561
"""
564562
Smx = s.transform_matrix
565563
Si = s.transform_matrix_inverse
566-
self.set_dense(_np.dot(Si, _np.dot(self.to_dense("minimal"), Smx)))
564+
self.set_dense(Si @ self.to_dense("minimal") @ Smx)
567565

568566
def spam_transform_inplace(self, s, typ):
569567
"""
@@ -591,9 +589,9 @@ def spam_transform_inplace(self, s, typ):
591589
None
592590
"""
593591
if typ == 'prep':
594-
self.set_dense(_np.dot(s.transform_matrix_inverse, self.to_dense("minimal")))
592+
self.set_dense(s.transform_matrix_inverse @ self.to_dense("minimal"))
595593
elif typ == 'effect':
596-
self.set_dense(_np.dot(self.to_dense("minimal"), s.transform_matrix))
594+
self.set_dense(self.to_dense("minimal") @ s.transform_matrix)
597595
else:
598596
raise ValueError("Invalid `typ` argument: %s" % typ)
599597

@@ -627,7 +625,7 @@ def depolarize(self, amount):
627625
else:
628626
assert(len(amount) == self.dim - 1)
629627
D = _np.diag([1] + list(1.0 - _np.array(amount, 'd')))
630-
self.set_dense(_np.dot(D, self.to_dense("minimal")))
628+
self.set_dense(D @ self.to_dense("minimal"))
631629

632630
def rotate(self, amount, mx_basis="gm"):
633631
"""
@@ -658,7 +656,7 @@ def rotate(self, amount, mx_basis="gm"):
658656
None
659657
"""
660658
rotnMx = _ot.rotation_gate_mx(amount, mx_basis)
661-
self.set_dense(_np.dot(rotnMx, self.to_dense("minimal")))
659+
self.set_dense(rotnMx @ self.to_dense("minimal"))
662660

663661
def deriv_wrt_params(self, wrt_filter=None):
664662
"""

pygsti/models/explicitcalc.py

Lines changed: 52 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,24 @@
2323

2424
from typing import Union
2525

26-
TransformMxPair = tuple[_np.ndarray, Union[_np.ndarray, _mt.IdentityOperator]]
26+
27+
TransformMxPair = tuple[
28+
Union[_np.ndarray, _mt.IdentityOperator],
29+
Union[_np.ndarray, _mt.IdentityOperator]
30+
]
31+
32+
33+
def to_transform_mx_pair(tmx_arg: Union[None, _np.ndarray, TransformMxPair]) -> TransformMxPair:
34+
if tmx_arg is None:
35+
P = invP = _mt.IdentityOperator()
36+
elif isinstance(tmx_arg, _np.ndarray):
37+
P = tmx_arg
38+
invP = _np.linalg.pinv(P)
39+
else:
40+
P, invP = tmx_arg
41+
assert _mt.is_operatorlike(P)
42+
assert _mt.is_operatorlike(invP)
43+
return P, invP
2744

2845
# Tolerace for matrix_rank when finding rank of a *normalized* projection
2946
# matrix. This is a legitimate tolerace since a normalized projection matrix
@@ -156,16 +173,7 @@ def frobeniusdist(self, other_calc, transform_mx: Union[None, _np.ndarray, Trans
156173
-------
157174
float
158175
"""
159-
if transform_mx is None:
160-
P, invP = None, None
161-
# ^ It would be equivalent to use P = invP = _mt.IdentityOperator()
162-
elif isinstance(transform_mx, _np.ndarray):
163-
P = transform_mx
164-
invP = _np.linalg.pinv(P)
165-
else:
166-
P, invP = transform_mx
167-
assert _mt.is_operatorlike(P)
168-
assert _mt.is_operatorlike(invP)
176+
P, invP = to_transform_mx_pair(transform_mx)
169177

170178
d = 0.0
171179
nSummands = 0.0
@@ -231,14 +239,12 @@ def residuals(self, other_calc, transform_mx=None, item_weights=None):
231239
The (weighted) number of elements accounted for by the residuals.
232240
"""
233241
resids = []
234-
T = transform_mx
235242
nSummands = 0.0
236243
if item_weights is None: item_weights = {}
237244
sqrt_itemWeights = {k: _np.sqrt(v) for k, v in item_weights.items()}
238245
opWeight = sqrt_itemWeights.get('gates', 1.0)
239246
spamWeight = sqrt_itemWeights.get('spam', 1.0)
240-
Ti = None if T is None else _np.linalg.pinv(T)
241-
# ^ TODO: generalize inverse op (call T.inverse() if T were a "transform" object?)
247+
T, Ti = to_transform_mx_pair(transform_mx)
242248

243249
for opLabel, gate in self.operations.items():
244250
wt = sqrt_itemWeights.get(opLabel, opWeight)
@@ -293,40 +299,23 @@ def jtracedist(self, other_calc, transform_mx=None, include_spam=True):
293299
-------
294300
float
295301
"""
296-
T = transform_mx
302+
T, Ti = to_transform_mx_pair(transform_mx)
297303
d = 0 # spam difference
298304
nSummands = 0 # for spam terms
299305

300-
if T is not None:
301-
Ti = _np.linalg.inv(T)
302-
dists = [gate.jtracedist(other_calc.operations[lbl], T, Ti)
303-
for lbl, gate in self.operations.items()]
304-
305-
#Just use frobenius distance between spam vecs, since jtracedist
306-
# doesn't really make sense
307-
if include_spam:
308-
for lbl, rhoV in self.preps.items():
309-
d += rhoV.frobeniusdist_squared(other_calc.preps[lbl], T, Ti)
310-
nSummands += rhoV.dim
306+
dists = [gate.jtracedist(other_calc.operations[lbl], T, Ti)
307+
for lbl, gate in self.operations.items()]
311308

312-
for lbl, Evec in self.effects.items():
313-
d += Evec.frobeniusdist_squared(other_calc.effects[lbl], T, Ti)
314-
nSummands += Evec.dim
309+
# Just use frobenius distance between spam vecs, since jtracedist
310+
# doesn't really make sense
311+
if include_spam:
312+
for lbl, rhoV in self.preps.items():
313+
d += rhoV.frobeniusdist_squared(other_calc.preps[lbl], T, Ti)
314+
nSummands += rhoV.dim
315315

316-
else:
317-
dists = [gate.jtracedist(other_calc.operations[lbl])
318-
for lbl, gate in self.operations.items()]
319-
320-
#Just use frobenius distance between spam vecs, since jtracedist
321-
# doesn't really make sense
322-
if include_spam:
323-
for lbl, rhoV in self.preps.items():
324-
d += rhoV.frobeniusdist_squared(other_calc.preps[lbl])
325-
nSummands += rhoV.dim
326-
327-
for lbl, Evec in self.effects.items():
328-
d += Evec.frobeniusdist_squared(other_calc.effects[lbl])
329-
nSummands += Evec.dim
316+
for lbl, Evec in self.effects.items():
317+
d += Evec.frobeniusdist_squared(other_calc.effects[lbl], T, Ti)
318+
nSummands += Evec.dim
330319

331320
spamVal = _np.sqrt(d / nSummands) if (nSummands > 0) else 0
332321
return max(dists) + spamVal
@@ -358,41 +347,24 @@ def diamonddist(self, other_calc, transform_mx=None, include_spam=True):
358347
-------
359348
float
360349
"""
361-
T = transform_mx
350+
T, Ti = to_transform_mx_pair(transform_mx)
362351
d = 0 # spam difference
363352
nSummands = 0 # for spam terms
364353

365-
if T is not None:
366-
Ti = _np.linalg.inv(T)
367-
dists = [gate.diamonddist(other_calc.operations[lbl], T, Ti)
368-
for lbl, gate in self.operations.items()]
354+
dists = [gate.diamonddist(other_calc.operations[lbl], T, Ti)
355+
for lbl, gate in self.operations.items()]
369356

370-
#Just use frobenius distance between spam vecs, since jtracedist
371-
# doesn't really make sense
372-
if include_spam:
373-
for lbl, rhoV in self.preps.items():
374-
d += rhoV.frobeniusdist_squared(other_calc.preps[lbl], T, Ti)
375-
nSummands += rhoV.dim
357+
# Just use frobenius distance between spam vecs, since diamonddist
358+
# doesn't really make sense
359+
if include_spam:
360+
for lbl, rhoV in self.preps.items():
361+
d += rhoV.frobeniusdist_squared(other_calc.preps[lbl], T, Ti)
362+
nSummands += rhoV.dim
376363

377-
for lbl, Evec in self.effects.items():
364+
for lbl, Evec in self.effects.items():
378365
d += Evec.frobeniusdist_squared(other_calc.effects[lbl], T, Ti)
379366
nSummands += Evec.dim
380367

381-
else:
382-
dists = [gate.diamonddist(other_calc.operations[lbl])
383-
for lbl, gate in self.operations.items()]
384-
385-
#Just use frobenius distance between spam vecs, since jtracedist
386-
# doesn't really make sense
387-
if include_spam:
388-
for lbl, rhoV in self.preps.items():
389-
d += rhoV.frobeniusdist_squared(other_calc.preps[lbl])
390-
nSummands += rhoV.dim
391-
392-
for lbl, Evec in self.effects.items():
393-
d += Evec.frobeniusdist_squared(other_calc.effects[lbl])
394-
nSummands += Evec.dim
395-
396368
spamVal = _np.sqrt(d / nSummands) if (nSummands > 0) else 0
397369
return max(dists) + spamVal
398370

@@ -420,7 +392,7 @@ def deriv_wrt_params(self):
420392
eo += obj.hilbert_schmidt_size
421393

422394
if self.interposer is not None:
423-
deriv = _np.dot(deriv, self.interposer.deriv_op_params_wrt_model_params())
395+
deriv = deriv @ self.interposer.deriv_op_params_wrt_model_params()
424396

425397
return deriv
426398

@@ -499,14 +471,13 @@ def _buildup_dpg(self):
499471
for j in range(dim): # *generator* mx, not gauge mx itself
500472
unitMx = _bc.mut(i, j, dim)
501473
for lbl, rhoVec in self_preps.items():
502-
mdlDeriv_preps[lbl] = _np.dot(unitMx, rhoVec)
474+
mdlDeriv_preps[lbl] = unitMx @ rhoVec
503475
for lbl, EVec in self_effects.items():
504-
mdlDeriv_effects[lbl] = -_np.dot(EVec.T, unitMx).T
476+
mdlDeriv_effects[lbl] = -(EVec.T @ unitMx).T
505477

506478
for lbl, gate in self_operations.items():
507479
#if isinstance(gate,_op.DenseOperator):
508-
mdlDeriv_ops[lbl] = _np.dot(unitMx, gate) - \
509-
_np.dot(gate, unitMx)
480+
mdlDeriv_ops[lbl] = (unitMx @ gate) - (gate @ unitMx)
510481
#else:
511482
# #use acton... maybe throw error if dim is too large (maybe above?)
512483
# deriv = _np.zeros((dim,dim),'d')
@@ -566,7 +537,7 @@ def nongauge_and_gauge_spaces(self, item_weights=None, non_gauge_mix_mx=None):
566537
#for each column of gen_dG, which is a gauge direction in model parameter space,
567538
# we add some amount of non-gauge direction, given by coefficients of the
568539
# numNonGaugeParams non-gauge directions.
569-
orthog_to = gauge_space + _np.dot(nonGaugeDirections, non_gauge_mix_mx) # add non-gauge components in
540+
orthog_to = gauge_space + nonGaugeDirections @ non_gauge_mix_mx # add non-gauge components in
570541
# dims: (nParams,n_gauge_params) + (nParams,n_non_gauge_params) * (n_non_gauge_params,n_gauge_params)
571542
# non_gauge_mix_mx is a (n_non_gauge_params,n_gauge_params) matrix whose i-th column specifies the
572543
# coefficients to multiply each of the non-gauge directions by before adding them to the i-th
@@ -583,7 +554,7 @@ def nongauge_and_gauge_spaces(self, item_weights=None, non_gauge_mix_mx=None):
583554
metric_diag[vec.gpindices] = item_weights.get(lbl, spamWeight)
584555
metric = _np.diag(metric_diag)
585556
#OLD: gen_ndG = _mt.nullspace(_np.dot(gen_dG.T,metric))
586-
orthog_to = _np.dot(metric.T, gauge_space)
557+
orthog_to = metric.T @ gauge_space
587558

588559
else:
589560
orthog_to = gauge_space
@@ -663,9 +634,9 @@ def _gauge_orbit_curvature(self, item_weights=None, non_gauge_mix_mx=None):
663634
unitMx_j = _bc.mut(j1, j2, dim)
664635
antiComm = (unitMx_i @ unitMx_j + unitMx_j @ unitMx_i)
665636
for lbl, rhoVec in self_preps.items():
666-
mdlHess_preps[lbl] = 0.5 * _np.dot(antiComm, rhoVec)
637+
mdlHess_preps[lbl] = 0.5 * (antiComm @ rhoVec)
667638
for lbl, EVec in self_effects.items():
668-
mdlHess_effects[lbl] = 0.5 * _np.dot(EVec.T, antiComm).T
639+
mdlHess_effects[lbl] = 0.5 * (EVec.T @ antiComm).T
669640
for lbl, gate in self_operations.items():
670641
mdlHess_ops[lbl] = 0.5 * (antiComm @ gate + gate @ antiComm) \
671642
- unitMx_i @ gate @ unitMx_j - unitMx_j @ gate @ unitMx_i
@@ -785,8 +756,8 @@ def nongauge_projector(self, item_weights=None, non_gauge_mix_mx=None):
785756

786757
# ORIG WAY: use pseudo-inverse to normalize projector. Ran into problems where
787758
# default rcond == 1e-15 didn't work for 2-qubit case, but still more stable than inv method below
788-
P = _np.dot(gen_ndG, _np.transpose(gen_ndG)) # almost a projector, but cols of dG are not orthonormal
789-
Pp = _np.dot(_np.linalg.pinv(P, rcond=1e-7), P) # make P into a true projector (onto gauge space)
759+
P = gen_ndG @ gen_ndG.T # almost a projector, but cols of dG are not orthonormal
760+
Pp = _np.linalg.pinv(P, rcond=1e-7) @ P # make P into a true projector (onto gauge space)
790761

791762
# ALT WAY: use inverse of dG^T*dG to normalize projector (see wikipedia on projectors, dG => A)
792763
# This *should* give the same thing as above, but numerical differences indicate the pinv method

0 commit comments

Comments
 (0)