Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix reparameterize #480

Open
wants to merge 25 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
627ec20
povm convert fixed for fullTP->GLND, missing CPTPLND considerations
juangmendoza19 Jul 16, 2024
484fe3e
full TP -> GLND reparam now avoids dumb-gauge, minimizes 2norm
juangmendoza19 Aug 8, 2024
6365db8
bypass our changes if reparam is not full TP -> GLND
juangmendoza19 Aug 16, 2024
246623e
removed magic numbers, adapted for arbitrary number of qubits
juangmendoza19 Aug 22, 2024
d4fd818
removed temp files
juangmendoza19 Aug 22, 2024
167d742
removed magic numbers from povm convert, and added a check if there i…
juangmendoza19 Aug 23, 2024
c6ec19f
commented povm file
juangmendoza19 Sep 4, 2024
2bf56d6
commented state file
juangmendoza19 Sep 4, 2024
0ec92dd
started implementing CPTPLND compatibility with set_all_parameterization
juangmendoza19 Sep 26, 2024
b769193
added support for converting to and from many other models. Tested th…
juangmendoza19 Dec 17, 2024
e8232b3
made cptp_penalty an optional argument, made it 1e-7 accross all inst…
juangmendoza19 Dec 17, 2024
5425f61
Merge remote-tracking branch 'origin/develop' into bugfix-reparameterize
juangmendoza19 Dec 18, 2024
d70fe7d
using proper warning library, started unit tests
juangmendoza19 Jan 3, 2025
51d75a5
unit test for process matrices in reparameterization added
juangmendoza19 Jan 6, 2025
d39ea28
fixed assert syntax
juangmendoza19 Jan 6, 2025
32970ff
fixed small bug in errgen parameter counting, now forward sim tests a…
juangmendoza19 Feb 20, 2025
6786b14
fixed warning bug in povm convert code
juangmendoza19 Feb 20, 2025
933425b
added proper criteria to test parameterization change
juangmendoza19 Feb 20, 2025
a737603
added comment to remember to add more tests in the future
juangmendoza19 Feb 20, 2025
16c7259
added param counting back to unit test
juangmendoza19 Feb 21, 2025
60e2673
removed incorrect formula use for errgen model unit test
juangmendoza19 Feb 21, 2025
abaff72
Merge branch 'develop' into bugfix-reparameterize
juangmendoza19 Feb 21, 2025
ad4b6ad
removed check for old cvxpy
juangmendoza19 Feb 21, 2025
7825c1b
added unit tests for parameterization changes starting from a GLND model
juangmendoza19 Feb 21, 2025
18b9294
implemented Corey's requests for the PR
juangmendoza19 Feb 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions pygsti/baseobjs/errorgenbasis.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,19 @@ def label_index(self, elemgen_label, ok_if_missing=False):
ok_if_missing : bool
If True, then returns `None` instead of an integer when the given label is not present.
"""
try:
return self.labels.index(elemgen_label)
except ValueError as error:

if ok_if_missing:
return None
else:
raise error
"""
TODO: 2 qubit labels returning None when label does exist in self.labels.
'(support, left_support) not in self._offsets[eetype]' returning True incorrectly


Copy link
Author

@juangmendoza19 juangmendoza19 Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fancy code below (now commented out) to find the index of a label was not working for 2 qubits. Temporary easy solution implemented by just calling index(). This was creating problems during the creation of 2 qubit FOGI models

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure that I implemented a similar fix for this on PR #538. Making note of this in case there are merge conflicts that need to eventually get resolved here.

support = elemgen_label.sslbls
eetype = elemgen_label.errorgen_type
bels = elemgen_label.basis_element_labels
Expand Down Expand Up @@ -490,6 +503,7 @@ def label_index(self, elemgen_label, ok_if_missing=False):
raise ValueError("Invalid elementary errorgen type: %s" % str(eetype))

return base + indices[elemgen_label]
"""

#@property
#def sslbls(self):
Expand Down
117 changes: 112 additions & 5 deletions pygsti/modelmembers/povms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
import _collections
import functools as _functools
import itertools as _itertools

import warnings
import numpy as _np
import scipy.linalg as _spl
import scipy.optimize as _spo

from .complementeffect import ComplementPOVMEffect
from .composedeffect import ComposedPOVMEffect
Expand All @@ -35,6 +37,8 @@
from pygsti.baseobjs import statespace as _statespace
from pygsti.tools import basistools as _bt
from pygsti.tools import optools as _ot
from pygsti.tools import sum_of_negative_choi_eigenvalues_gate
from pygsti.baseobjs import Basis

# Avoid circular import
import pygsti.modelmembers as _mm
Expand Down Expand Up @@ -295,7 +299,7 @@ def povm_type_from_op_type(op_type):
return povm_type_preferences


def convert(povm, to_type, basis, ideal_povm=None, flatten_structure=False):
def convert(povm, to_type, basis, cptp_penalty=1e-7, ideal_povm=None, flatten_structure=False):
"""
TODO: update docstring
Convert a POVM to a new type of parameterization.
Expand Down Expand Up @@ -327,13 +331,19 @@ def convert(povm, to_type, basis, ideal_povm=None, flatten_structure=False):
are separately converted, leaving the original POVM's structure
unchanged. When `True`, composed and embedded operations are "flattened"
into a single POVM of the requested `to_type`.

cptp_penalty : float, optional (default 1e-7)
Converting SPAM operations to error generator types includes extra degrees of gauge freedom (dumb gauge).
Copy link
Contributor

@coreyostrove coreyostrove Feb 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not eternalize this naming choice in the documentation. If there are other places this was introduced take this comment as a blanket request to change those. Could you switch this to something like 'trivial gauge', or something else? Here is a candidate for new docstring:

Converting SPAM operations to an error generator representation may introduce trivial gauge degrees of freedom. These gauge degrees of freedom are called trivial because they quite literally do not change the dense representation (i.e. Hilbert-Schmidt vectors) at all. Despite being trivial, error generators along this trivial gauge orbit may be non-CP, so this cptp penalty is used to favor channels within this gauge orbit which are CPTP.

I'll also note that strictly speaking this is only a CP penalty, as TP is enforced by construction when using elementary error generators, but that is mostly a pedantic comment. You can leave the name alone.

This cptp penalty is used to favor channels within this gauge orbit which are CPTP.

Returns
-------
POVM
The converted POVM vector, usually a distinct
object from the object passed as input.
"""

##TEST CONVERSION BETWEEN LINBLAD TYPES
to_types = to_type if isinstance(to_type, (tuple, list)) else (to_type,) # HACK to support multiple to_type values
error_msgs = {}

Expand Down Expand Up @@ -364,6 +374,7 @@ def convert(povm, to_type, basis, ideal_povm=None, flatten_structure=False):
from ..operations import LindbladParameterization as _LindbladParameterization
lndtype = _LindbladParameterization.cast(to_type)


#Construct a static "base" POVM
if isinstance(povm, ComputationalBasisPOVM): # special easy case
base_povm = ComputationalBasisPOVM(povm.state_space.num_qubits, povm.evotype) # just copy it?
Expand All @@ -375,14 +386,110 @@ def convert(povm, to_type, basis, ideal_povm=None, flatten_structure=False):
for lbl, vec in povm.items()]
else:
raise RuntimeError('Evotype must be compatible with Hilbert ops to use pure effects')
except Exception: # try static mixed states next:
base_items = [(lbl, convert_effect(vec, 'static', basis, idl.get(lbl, None), flatten_structure))
for lbl, vec in povm.items()]
except RuntimeError: # try static mixed states next:
#if idl.get(lbl,None) is not None:

base_items = []
for lbl, vec in povm.items():
ideal_effect = idl.get(lbl,None)
if ideal_effect is not None:
base_items.append((lbl, convert_effect(ideal_effect, 'static', basis, ideal_effect, flatten_structure)))
else:
base_items.append((lbl, convert_effect(vec, 'static', basis, idl.get(lbl, None), flatten_structure)))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is code to properly collect the ideal POVM effects. Before, the ideal_effect passed in was being ignored

base_povm = UnconstrainedPOVM(base_items, povm.evotype, povm.state_space)

proj_basis = 'PP' if povm.state_space.is_entirely_qubits else basis
errorgen = _LindbladErrorgen.from_error_generator(povm.state_space.dim, lndtype, proj_basis,
basis, truncate=True, evotype=povm.evotype)
#if to_type == 'GLND' and isinstance(povm, destination_types.get('full TP', NoneType)):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commented out line looks like it can be safely deleted.


#Collect all ideal effects
base_dense_effects = []
for item in base_items:
dense_effect = item[1].to_dense()
base_dense_effects.append(dense_effect.reshape((1,len(dense_effect))))

dense_ideal_povm = _np.concatenate(base_dense_effects, axis=0)

#Collect all noisy effects
dense_effects = []
for effect in povm.values():
dense_effect = effect.to_dense()
dense_effects.append(dense_effect.reshape((1,len(dense_effect))))

dense_povm = _np.concatenate(dense_effects, axis=0)
degrees_of_freedom = (dense_ideal_povm.shape[0] - 1) * dense_ideal_povm.shape[1]



#It is often the case that there are more error generators than physical degrees of freedom in the POVM
#We define a function which finds linear comb. of errgens that span these degrees of freedom.
#This has been called "the dumb gauge", and this function is meant to avoid it
def calc_physical_subspace(dense_ideal_povm, epsilon = 1e-9):

errgen = _LindbladErrorgen.from_error_generator(povm.state_space.dim, parameterization=to_type)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless this is just a visual artifact on github it looks like you've double indented this function block. Could you fix that?

if degrees_of_freedom > errgen.num_params:
warnings.warn("POVM has more degrees of freedom than the available number of parameters, representation in this parameterization is not guaranteed")
exp_errgen = _ExpErrorgenOp(errgen)

num_errgens = errgen.num_params
#TODO: Maybe we can use the num of params instead of number of matrix entries, as some of them are linearly dependent.
#i.e E0 completely determines E1 if those are the only two povm elements (E0 + E1 = Identity)
num_entries = dense_ideal_povm.shape[0]*dense_ideal_povm.shape[1]
#assert num_errgens >= povm.num_params, "POVM has too many elements, error generator parameterization is not possible"

ideal_vec = _np.zeros(num_errgens)

#Compute the jacobian with respect to the error generators. This will allow us to see which
#error generators change the POVM entries
J = _np.zeros((num_entries,num_errgens))

for i in range(len(ideal_vec)):
new_vec = ideal_vec.copy()
new_vec[i] = epsilon
exp_errgen.from_vector(new_vec)
vectorized_povm = _np.zeros(num_entries)
perturbed_povm = (dense_ideal_povm @ exp_errgen.to_dense() - dense_ideal_povm)/epsilon

perturbed_povm_t = perturbed_povm.transpose()
for j, column in enumerate(perturbed_povm_t):
vectorized_povm[j*len(perturbed_povm_t[0]):(j+1)*len(perturbed_povm_t[0])] = column

J[:,i] = vectorized_povm.transpose()

_,S,V = _np.linalg.svd(J)
return V[:len(S),]

phys_directions = calc_physical_subspace(dense_ideal_povm)

#We use optimization to find the best error generator representation
#we only vary physical directions, not independent error generators
dense_basis = Basis.cast('pp', povm.state_space.dim)
def _objfn(v):

#For some reason adding the sum_of_negative_choi_eigenvalues_gate term
#resulted in minimize() sometimes choosing NaN values for v. There are
#two stack exchange issues showing this problem with no solution.
if _np.isnan(v).any():
v = _np.zeros(len(v))

L_vec = _np.zeros(len(phys_directions[0]))
for coeff, phys_direction in zip(v,phys_directions):
L_vec += coeff * phys_direction
errorgen.from_vector(L_vec)
proc_matrix = _spl.expm(errorgen.to_dense())

return _np.linalg.norm(dense_povm - dense_ideal_povm @ proc_matrix) + cptp_penalty * sum_of_negative_choi_eigenvalues_gate(proc_matrix, dense_basis)

#def callback(x): print("callbk: ",_np.linalg.norm(x),_objfn(x)) # REMOVE
soln = _spo.minimize(_objfn, _np.zeros(len(phys_directions), 'd'), method="Nelder-Mead", options={},
tol=1e-13) # , callback=callback)
#print("DEBUG: opt done: ",soln.success, soln.fun, soln.x) # REMOVE
if not soln.success and soln.fun > 1e-6: # not "or" because success is often not set correctly
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the two REMOVE lines.

raise ValueError("Failed to find an errorgen such that <ideal|exp(errorgen) = <effect|")
errgen_vec = _np.linalg.pinv(phys_directions) @ soln.x
errorgen.from_vector(errgen_vec)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code added to find errgen parameterization for POVMs based on optimization, just like state prep was being done before, but for some reason was never implemented on POVMs. Note that the optimization variations are done over a POVM space that lacks the dumb-gauge/metagauge. To find that space we wrote the function calc_physical_subspace which does a numerical calculation of the Jacobian with respect to POVM parameters.

EffectiveExpErrorgen = _IdentityPlusErrorgenOp if lndtype.meta == '1+' else _ExpErrorgenOp
return ComposedPOVM(EffectiveExpErrorgen(errorgen), base_povm, mx_basis=basis)

Expand Down
60 changes: 51 additions & 9 deletions pygsti/modelmembers/states/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
from .tpstate import TPState
from pygsti.baseobjs import statespace as _statespace
from pygsti.tools import basistools as _bt
from pygsti.baseobjs import Basis
from pygsti.tools import optools as _ot
from pygsti.tools import sum_of_negative_choi_eigenvalues_gate

# Avoid circular import
import pygsti.modelmembers as _mm
Expand Down Expand Up @@ -185,7 +187,7 @@ def state_type_from_op_type(op_type):
return state_type_preferences


def convert(state, to_type, basis, ideal_state=None, flatten_structure=False):
def convert(state, to_type, basis, cptp_penalty=1e-7, ideal_state=None, flatten_structure=False):
"""
TODO: update docstring
Convert SPAM vector to a new type of parameterization.
Expand Down Expand Up @@ -217,6 +219,10 @@ def convert(state, to_type, basis, ideal_state=None, flatten_structure=False):
are separately converted, leaving the original state's structure
unchanged. When `True`, composed and embedded operations are "flattened"
into a single state of the requested `to_type`.

cptp_penalty : float, optional
CPTP penalty that gets factored into the optimization to find the resulting model
when converting to an error-generator type.

Returns
-------
Expand All @@ -234,7 +240,6 @@ def convert(state, to_type, basis, ideal_state=None, flatten_structure=False):
'static unitary': StaticPureState,
'static clifford': ComputationalBasisState}
NoneType = type(None)

for to_type in to_types:
try:
if isinstance(state, destination_types.get(to_type, NoneType)):
Expand All @@ -256,20 +261,57 @@ def convert(state, to_type, basis, ideal_state=None, flatten_structure=False):
errorgen = _LindbladErrorgen.from_error_generator(state.state_space.dim, to_type, proj_basis,
basis, truncate=True, evotype=state.evotype)
if st is not state and not _np.allclose(st.to_dense(), state.to_dense()):
#Need to set errorgen so exp(errorgen)|st> == |state>

dense_st = st.to_dense()
dense_state = state.to_dense()
num_qubits = st.state_space.num_qubits

errgen = _LindbladErrorgen.from_error_generator(2**(2*num_qubits), parameterization=to_type)
num_errgens = errgen.num_params

#GLND for states suffers from "dumb gauge" freedom. This function identifies
#the physical directions to avoid this gauge.
def calc_physical_subspace(ideal_prep, epsilon = 1e-9):

exp_errgen = _ExpErrorgenOp(errgen)
ideal_vec = _np.zeros(num_errgens)

#Compute the jacobian with respect to the error generators. This will allow us to see which
#error generators change the POVM entries
J = _np.zeros((state.num_params, num_errgens))

for i in range(len(ideal_vec)):
new_vec = ideal_vec.copy()
new_vec[i] = epsilon
exp_errgen.from_vector(new_vec)
J[:,i] = (exp_errgen.to_dense() @ ideal_prep - ideal_prep)[1:]/epsilon

_,S,V = _np.linalg.svd(J)
return V[:len(S),]

phys_directions = calc_physical_subspace(dense_state)

#We use optimization to find the best error generator representation
#we only vary physical directions, not independent error generators
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just as explained in the POVM init file, we added a function to identify the dumb-gauge/metagauge and move orthogonally to it. This allows for better optimization.


def _objfn(v):
errorgen.from_vector(v)
return _np.linalg.norm(_spl.expm(errorgen.to_dense()) @ dense_st - dense_state)
#def callback(x): print("callbk: ",_np.linalg.norm(x),_objfn(x)) # REMOVE
soln = _spo.minimize(_objfn, _np.zeros(errorgen.num_params, 'd'), method="CG", options={},
tol=1e-8) # , callback=callback)
L_vec = _np.zeros(len(phys_directions[0]))
for coeff, phys_direction in zip(v,phys_directions):
L_vec += coeff * phys_direction
errorgen.from_vector(L_vec)
proc_matrix = _spl.expm(errorgen.to_dense())
#basis of to_dense() CHECK if always pp
dense_basis = Basis.cast('pp', (2**num_qubits)**2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't assume a hardcoded pauli-product basis. I don't think the error generator modelmember itself has a basis attribute to grab, but it should have a reference to a parent model which would have this available. Can you update this to grab the appropriate basis? (And if you were doing something similar for the POVMs and I missed it add the change there too.)

return _np.linalg.norm(proc_matrix @ dense_st - dense_state) + cptp_penalty * sum_of_negative_choi_eigenvalues_gate(proc_matrix, dense_basis)

soln = _spo.minimize(_objfn, _np.zeros(len(phys_directions), 'd'), method="Nelder-Mead", options={},
tol=1e-13) # , callback=callback)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the objective function to be optimized over was modified to work with linear combinations of error generators which lack these dumb-gauge degrees of freedom

#print("DEBUG: opt done: ",soln.success, soln.fun, soln.x) # REMOVE
if not soln.success and soln.fun > 1e-6: # not "or" because success is often not set correctly
raise ValueError("Failed to find an errorgen such that exp(errorgen)|ideal> = |state>")
errorgen.from_vector(soln.x)

errgen_vec = _np.linalg.pinv(phys_directions) @ soln.x
errorgen.from_vector(errgen_vec)

EffectiveExpErrorgen = _IdentityPlusErrorgenOp if lndtype.meta == '1+' else _ExpErrorgenOp
return ComposedState(st, EffectiveExpErrorgen(errorgen))
Expand Down
Loading
Loading