Skip to content

Commit cd9fc35

Browse files
authored
Manually handle failed auto-merge of bugfix into develop (#675)
The automatic ``develop <- bugfix`` merge from Corey's PR yesterday (#674) [failed](https://github.com/sandialabs/pyGSTi/actions/runs/18768908655/job/53549598808#step:3:20). The current state of bugfix includes changes from #674 and the PR I just merged, #672. There's no way that the automatic ``develop <- bugfix`` merge will work since it includes the changes from yesterday that failed the auto-merge. So this PR catches develop up with collective changes in #672 and #674. Tests for the candidate ``develop <- bugfix`` merge workflow passed (all that failed was the auto-merge), so I'm going to merge this PR without waiting for tests to pass. Notes: It's not clear to me why the auto-merge for #674 failed. It might have something to do with the weird commit history I call out in the screenshot below. <img width="1168" height="204" alt="image" src="https://github.com/user-attachments/assets/d8f196f8-26f6-45d1-b77f-e74c63e0a0d1" />
2 parents 9910707 + b5feea1 commit cd9fc35

File tree

6 files changed

+223
-152
lines changed

6 files changed

+223
-152
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)