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 8 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
92 changes: 89 additions & 3 deletions pygsti/modelmembers/povms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import itertools as _itertools

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

from .complementeffect import ComplementPOVMEffect
from .composedeffect import ComposedPOVMEffect
Expand Down Expand Up @@ -334,6 +336,8 @@ def convert(povm, to_type, basis, ideal_povm=None, flatten_structure=False):
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 @@ -375,14 +379,96 @@ 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)):

#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_ideal_povm = _np.concatenate(base_dense_effects, axis=0)
dense_povm = _np.concatenate(dense_effects, axis=0)

#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(4, parameterization="GLND")
exp_errgen = _ExpErrorgenOp(errgen)
num_qubits = povm.state_space.num_qubits
num_errgens = 2**(4*num_qubits)-2**(2*num_qubits)
#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, GLND 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
def _objfn(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)
return _np.linalg.norm(dense_povm - dense_ideal_povm @ _spl.expm(errorgen.to_dense()))

#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
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
46 changes: 39 additions & 7 deletions pygsti/modelmembers/states/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,20 +258,52 @@ 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
num_errgens = 2**(4*num_qubits)-2**(2*num_qubits)

#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):

errgen = _LindbladErrorgen.from_error_generator(2**(2*num_qubits), parameterization="GLND")
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)
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)
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)

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
2 changes: 1 addition & 1 deletion pygsti/models/modelparaminterposer.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def __init__(self, transform_matrix):
self.transform_matrix = transform_matrix # cols specify a model parameter in terms of op params.
self.inv_transform_matrix = _np.linalg.pinv(self.transform_matrix)
super().__init__(transform_matrix.shape[1], transform_matrix.shape[0])

def model_paramvec_to_ops_paramvec(self, v):
return _np.dot(self.transform_matrix, v)

Expand Down