def test_sparse_lindblad_bases(self): sparsePP = Basis.cast("pp", 16, sparse=True) mxs = sparsePP.elements for lbl, mx in zip(sparsePP.labels, mxs): print("{}: {} matrix with {} nonzero entries (of {} total)".format( lbl, mx.shape, mx.nnz, mx.shape[0] * mx.shape[1])) print(mx.toarray()) print("{} basis elements".format(len(sparsePP))) self.assertEqual(len(sparsePP), 16) # TODO assert correctness M = np.ones((16, 16), 'd') v = np.ones(16, 'd') S = scipy.sparse.identity(16, 'd', 'csr') print("Test types after basis change by sparse basis:") Mout = bt.change_basis(M, sparsePP, 'std') vout = bt.change_basis(v, sparsePP, 'std') Sout = bt.change_basis(S, sparsePP, 'std') print("{} -> {}".format(type(M), type(Mout))) print("{} -> {}".format(type(v), type(vout))) print("{} -> {}".format(type(S), type(Sout))) self.assertIsInstance(Mout, np.ndarray) self.assertIsInstance(vout, np.ndarray) self.assertIsInstance(Sout, scipy.sparse.csr_matrix)
def test_general(self): std = Basis.cast('std', 4) std4 = Basis.cast('std', 16) std2x2 = Basis.cast([('std', 4), ('std', 4)]) gm = Basis.cast('gm', 4) from_basis, to_basis = bt.build_basis_pair(np.identity(4, 'd'), "std", "gm") from_basis, to_basis = bt.build_basis_pair(np.identity(4, 'd'), std, "gm") from_basis, to_basis = bt.build_basis_pair(np.identity(4, 'd'), "std", gm) mx = np.array([[1, 0, 0, 1], [0, 1, 2, 0], [0, 2, 1, 0], [1, 0, 0, 1]]) bt.change_basis(mx, 'std', 'gm') # shortname lookup bt.change_basis(mx, std, gm) # object bt.change_basis(mx, std, 'gm') # combination bt.flexible_change_basis(mx, std, gm) # same dimension I2x2 = np.identity(8, 'd') I4 = bt.flexible_change_basis(I2x2, std2x2, std4) self.assertArraysAlmostEqual( bt.flexible_change_basis(I4, std4, std2x2), I2x2) with self.assertRaises(AssertionError): bt.change_basis(mx, std, std4) # basis size mismatch mxInStdBasis = np.array( [[1, 0, 0, 2], [0, 0, 0, 0], [0, 0, 0, 0], [3, 0, 0, 4]], 'd') begin = Basis.cast('std', [1, 1]) end = Basis.cast('std', 4) mxInReducedBasis = bt.resize_std_mx(mxInStdBasis, 'contract', end, begin) original = bt.resize_std_mx(mxInReducedBasis, 'expand', begin, end)
def to_dense(self, on_space='minimal', scratch=None): """ Return this state vector as a (dense) numpy array. The memory in `scratch` maybe used when it is not-None. Parameters ---------- on_space : {'minimal', 'Hilbert', 'HilbertSchmidt'} The space that the returned dense operation acts upon. For unitary matrices and bra/ket vectors, use `'Hilbert'`. For superoperator matrices and super-bra/super-ket vectors use `'HilbertSchmidt'`. `'minimal'` means that `'Hilbert'` is used if possible given this operator's evolution type, and otherwise `'HilbertSchmidt'` is used. scratch : numpy.ndarray, optional scratch space available for use. Returns ------- numpy.ndarray """ assert (on_space in ('minimal', 'HilbertSchmidt')) dmVec_std = _ot.state_to_dmvec( self.pure_state.to_dense(on_space='Hilbert')) return _bt.change_basis(dmVec_std, 'std', self.basis)
def create_target_gate(self, v): phi, theta = v target_unitary = (np.cos(theta / 2) * sigI + 1.j * np.sin(theta / 2) * (np.cos(phi) * sigX + np.sin(phi) * sigY)) superop = change_basis(np.kron(target_unitary.conj(), target_unitary), 'col', 'pp') return superop
def jamiolkowski_iso_inv(choi_mx, choi_mx_basis='pp', op_mx_basis='pp'): """ Given a choi matrix, return the corresponding operation matrix. This function performs the inverse of :function:`jamiolkowski_iso`. Parameters ---------- choi_mx : numpy array the Choi matrix, normalized to have trace == 1, to compute operation matrix for. choi_mx_basis : Basis object The source and destination basis, respectively. Allowed values are Matrix-unit (std), Gell-Mann (gm), Pauli-product (pp), and Qutrit (qt) (or a custom basis object). op_mx_basis : Basis object The source and destination basis, respectively. Allowed values are Matrix-unit (std), Gell-Mann (gm), Pauli-product (pp), and Qutrit (qt) (or a custom basis object). Returns ------- numpy array operation matrix in the desired basis. """ choi_mx = _np.asarray(choi_mx) # will have "expanded" dimension even if bases are for reduced... N = choi_mx.shape[0] # dimension of full-basis (expanded) operation matrix if not isinstance(choi_mx_basis, _Basis): # if we're not given a basis, build choi_mx_basis = _Basis.cast(choi_mx_basis, N) # one with the full dimension dmDim = int(round(_np.sqrt(N))) # density matrix dimension #get full list of basis matrices (in std basis) BVec = _bt.basis_matrices(choi_mx_basis.create_simple_equivalent(), N) assert(len(BVec) == N) # make sure the number of basis matrices matches the dim of the choi matrix given # Invert normalization choiMx_unnorm = choi_mx * dmDim opMxInStdBasis = _np.zeros((N, N), 'complex') # in matrix unit basis of entire density matrix for i in range(N): for j in range(N): BiBj = _np.kron(BVec[i], _np.conjugate(BVec[j])) opMxInStdBasis += choiMx_unnorm[i, j] * BiBj if not isinstance(op_mx_basis, _Basis): op_mx_basis = _Basis.cast(op_mx_basis, N) # make sure op_mx_basis is a Basis; we'd like dimension to be N #project operation matrix so it acts only on the space given by the desired state space blocks opMxInStdBasis = _bt.resize_std_mx(opMxInStdBasis, 'contract', op_mx_basis.create_simple_equivalent('std'), op_mx_basis.create_equivalent('std')) #transform operation matrix into appropriate basis return _bt.change_basis(opMxInStdBasis, op_mx_basis.create_equivalent('std'), op_mx_basis)
def create_process_matrix(self, v, comm=None, return_generator=False): processes = [] phi, theta, t = v theta = theta * t target_unitary = (np.cos(theta / 2) * sigI + 1.j * np.sin(theta / 2) * (np.cos(phi) * sigX + np.sin(phi) * sigY)) superop = change_basis(np.kron(target_unitary.conj(), target_unitary), 'col', 'pp') processes += [superop] return np.array(processes) if (processes is not None) else None
def create_effect_from_dmvec(superket_vector, effect_type, basis='pp', evotype='default', state_space=None, on_construction_error='warn'): effect_type_preferences = (effect_type,) if isinstance(effect_type, str) else effect_type if state_space is None: state_space = _statespace.default_space_for_dim(len(superket_vector)) for typ in effect_type_preferences: try: if typ == "static": ef = StaticPOVMEffect(superket_vector, evotype, state_space) elif typ == "full": ef = FullPOVMEffect(superket_vector, evotype, state_space) elif _ot.is_valid_lindblad_paramtype(typ): from ..operations import LindbladErrorgen as _LindbladErrorgen, ExpErrorgenOp as _ExpErrorgenOp try: dmvec = _bt.change_basis(superket_vector, basis, 'std') purevec = _ot.dmvec_to_state(dmvec) # raises error if dmvec does not correspond to a pure state static_effect = StaticPOVMPureEffect(purevec, basis, evotype, state_space) except ValueError: static_effect = StaticPOVMEffect(superket_vector, evotype, state_space) proj_basis = 'PP' if state_space.is_entirely_qubits else basis errorgen = _LindbladErrorgen.from_error_generator(state_space.dim, typ, proj_basis, basis, truncate=True, evotype=evotype) ef = ComposedPOVMEffect(static_effect, _ExpErrorgenOp(errorgen)) else: # Anything else we try to convert to a pure vector and convert the pure state vector dmvec = _bt.change_basis(superket_vector, basis, 'std') purevec = _ot.dmvec_to_state(dmvec) # raises error if dmvec does not correspond to a pure state ef = create_effect_from_pure_vector(purevec, typ, basis, evotype, state_space) return ef except (ValueError, AssertionError) as err: if on_construction_error == 'raise': raise err elif on_construction_error == 'warn': print('Failed to construct effect with type "{}" with error: {}'.format(typ, str(err))) pass # move on to next type raise ValueError("Could not create an effect of type(s) %s from the given superket vector!" % (str(effect_type)))
def create_process_matrices(self, v, grouped_v, comm=None): assert (len(grouped_v) == 1) # we expect a single "grouped" parameter processes = [] times = grouped_v[0] phi_in, theta_in = v for t in times: phi = phi_in theta = theta_in * t target_unitary = (np.cos(theta / 2) * sigI + 1.j * np.sin(theta / 2) * (np.cos(phi) * sigX + np.sin(phi) * sigY)) superop = change_basis( np.kron(target_unitary.conj(), target_unitary), 'col', 'pp') processes += [superop] return np.array(processes) if (processes is not None) else None
def fast_jamiolkowski_iso_std(operation_mx, op_mx_basis): """ The corresponding Choi matrix in the standard basis that is normalized to have trace == 1. This routine *only* computes the case of the Choi matrix being in the standard (matrix unit) basis, but does so more quickly than :func:`jamiolkowski_iso` and so is particuarly useful when only the eigenvalues of the Choi matrix are needed. Parameters ---------- operation_mx : numpy array the operation matrix to compute Choi matrix of. op_mx_basis : Basis object The source and destination basis, respectively. Allowed values are Matrix-unit (std), Gell-Mann (gm), Pauli-product (pp), and Qutrit (qt) (or a custom basis object). Returns ------- numpy array the Choi matrix, normalized to have trace == 1, in the std basis. """ #first, get operation matrix into std basis operation_mx = _np.asarray(operation_mx) op_mx_basis = _bt.create_basis_for_matrix(operation_mx, op_mx_basis) opMxInStdBasis = _bt.change_basis(operation_mx, op_mx_basis, op_mx_basis.create_equivalent('std')) #expand operation matrix so it acts on entire space of dmDim x dmDim density matrices opMxInStdBasis = _bt.resize_std_mx(opMxInStdBasis, 'expand', op_mx_basis.create_equivalent('std'), op_mx_basis.create_simple_equivalent('std')) #Shuffle indices to go from process matrix to Jamiolkowski matrix (they vectorize differently) N2 = opMxInStdBasis.shape[0]; N = int(_np.sqrt(N2)) assert(N * N == N2) # make sure N2 is a perfect square Jmx = opMxInStdBasis.reshape((N, N, N, N)) Jmx = _np.swapaxes(Jmx, 1, 2).flatten() Jmx = Jmx.reshape((N2, N2)) # This construction results in a Jmx with trace == dim(H) = sqrt(gateMxInPauliBasis.shape[0]) # but we'd like a Jmx with trace == 1, so normalize: Jmx_norm = Jmx / N return Jmx_norm
def create_from_dmvecs(superket_vectors, povm_type, basis='pp', evotype='default', state_space=None, on_construction_error='warn'): """ TODO: docstring -- create a POVM from a list/dict of (key, pure-vector) pairs """ povm_type_preferences = (povm_type,) if isinstance(povm_type, str) else povm_type if not isinstance(superket_vectors, dict): # then assume it's a list of (key, value) pairs superket_vectors = _collections.OrderedDict(superket_vectors) for typ in povm_type_preferences: try: if typ in ("full", "static"): effects = [(lbl, create_effect_from_dmvec(dmvec, typ, basis, evotype, state_space)) for lbl, dmvec in superket_vectors.items()] povm = UnconstrainedPOVM(effects, evotype, state_space) elif typ == 'full TP': effects = [(lbl, create_effect_from_dmvec(dmvec, 'full', basis, evotype, state_space)) for lbl, dmvec in superket_vectors.items()] povm = TPPOVM(effects, evotype, state_space) elif _ot.is_valid_lindblad_paramtype(typ): from ..operations import LindbladErrorgen as _LindbladErrorgen, ExpErrorgenOp as _ExpErrorgenOp base_povm = create_from_dmvecs(superket_vectors, ('computational', 'static'), basis, evotype, state_space) proj_basis = 'PP' if state_space.is_entirely_qubits else basis errorgen = _LindbladErrorgen.from_error_generator(state_space.dim, typ, proj_basis, basis, truncate=True, evotype=evotype, state_space=state_space) povm = ComposedPOVM(_ExpErrorgenOp(errorgen), base_povm, mx_basis=basis) elif typ in ('computational', 'static pure', 'full pure'): # RESHAPE NOTE: .flatten() added to line below (to convert pure *col* vec -> 1D) to fix unit tests pure_vectors = {k: _ot.dmvec_to_state(_bt.change_basis(superket, basis, 'std')).flatten() for k, superket in superket_vectors.items()} povm = create_from_pure_vectors(pure_vectors, typ, basis, evotype, state_space) else: raise ValueError("Unknown POVM type '%s'!" % str(typ)) return povm # if we get to here, then we've successfully created a state to return except (ValueError, AssertionError) as err: if on_construction_error == 'raise': raise err elif on_construction_error == 'warn': print('Failed to construct povm with type "{}" with error: {}'.format(typ, str(err))) pass # move on to next type raise ValueError("Could not create a POVM of type(s) %s from the given pure vectors!" % (str(povm_type)))
def test_unitary_to_pauligate(self): theta = np.pi sigmax = np.array([[0, 1], [1, 0]]) ex = 1j * theta * sigmax / 2 U = scipy.linalg.expm(ex) # U is 2x2 unitary matrix operating on single qubit in [0,1] basis (X(pi) rotation) op = ot.unitary_to_pauligate(U) op_ans = np.array([[1., 0., 0., 0.], [0., 1., 0., 0.], [0., 0., -1., 0.], [0., 0., 0., -1.]], 'd') self.assertArraysAlmostEqual(op, op_ans) U_2Q = np.identity(4, 'complex') U_2Q[2:, 2:] = U # U_2Q is 4x4 unitary matrix operating on isolated two-qubit space (CX(pi) rotation) op_2Q = ot.unitary_to_pauligate(U_2Q) op_2Q_inv = ot.process_mx_to_unitary( bt.change_basis(op_2Q, 'pp', 'std')) self.assertArraysAlmostEqual(U_2Q, op_2Q_inv)
def fast_jamiolkowski_iso_std_inv(choi_mx, op_mx_basis): """ Given a choi matrix in the standard basis, return the corresponding operation matrix. This function performs the inverse of :function:`fast_jamiolkowski_iso_std`. Parameters ---------- choi_mx : numpy array the Choi matrix in the standard (matrix units) basis, normalized to have trace == 1, to compute operation matrix for. op_mx_basis : Basis object The source and destination basis, respectively. Allowed values are Matrix-unit (std), Gell-Mann (gm), Pauli-product (pp), and Qutrit (qt) (or a custom basis object). Returns ------- numpy array operation matrix in the desired basis. """ #Shuffle indices to go from process matrix to Jamiolkowski matrix (they vectorize differently) N2 = choi_mx.shape[0]; N = int(_np.sqrt(N2)) assert(N * N == N2) # make sure N2 is a perfect square opMxInStdBasis = choi_mx.reshape((N, N, N, N)) * N opMxInStdBasis = _np.swapaxes(opMxInStdBasis, 1, 2).flatten() opMxInStdBasis = opMxInStdBasis.reshape((N2, N2)) op_mx_basis = _bt.create_basis_for_matrix(opMxInStdBasis, op_mx_basis) #project operation matrix so it acts only on the space given by the desired state space blocks opMxInStdBasis = _bt.resize_std_mx(opMxInStdBasis, 'contract', op_mx_basis.create_simple_equivalent('std'), op_mx_basis.create_equivalent('std')) #transform operation matrix into appropriate basis return _bt.change_basis(opMxInStdBasis, op_mx_basis.create_equivalent('std'), op_mx_basis)
def test_lind_errgen_projects(self): mx_basis = Basis.cast('pp', 4) basis = Basis.cast('PP', 4) X = basis['X'] Y = basis['Y'] Z = basis['Z'] # Build known combination to project back to errgen = 0.1 * lt.create_elementary_errorgen('H', Z) \ - 0.01 * lt.create_elementary_errorgen('H', X) \ + 0.2 * lt.create_elementary_errorgen('S', X) \ + 0.25 * lt.create_elementary_errorgen('S', Y) \ + 0.05 * lt.create_elementary_errorgen('C', X, Y) \ - 0.01 * lt.create_elementary_errorgen('A', X, Y) errgen = bt.change_basis(errgen, 'std', mx_basis) Hblk = LindbladCoefficientBlock('ham', basis) ODblk = LindbladCoefficientBlock('other_diagonal', basis) Oblk = LindbladCoefficientBlock('other', basis) Hblk.set_from_errorgen_projections(errgen, errorgen_basis=mx_basis) ODblk.set_from_errorgen_projections(errgen, errorgen_basis=mx_basis) Oblk.set_from_errorgen_projections(errgen, errorgen_basis=mx_basis) self.assertArraysAlmostEqual(Hblk.block_data, [-0.01, 0, 0.1]) self.assertArraysAlmostEqual(ODblk.block_data, [0.2, 0.25, 0]) self.assertArraysAlmostEqual( Oblk.block_data, np.array([[0.2, 0.05 + 0.01j, 0], [0.05 - 0.01j, 0.25, 0], [0, 0, 0]])) def dicts_equal(d, f): f = {LEEL.cast(k): v for k, v in f.items()} if set(d.keys()) != set(f.keys()): return False for k in d: if abs(d[k] - f[k]) > 1e-12: return False return True self.assertTrue( dicts_equal(Hblk.elementary_errorgens, { ('H', 'Z'): 0.1, ('H', 'X'): -0.01, ('H', 'Y'): 0 })) self.assertTrue( dicts_equal(ODblk.elementary_errorgens, { ('S', 'X'): 0.2, ('S', 'Y'): 0.25, ('S', 'Z'): 0 })) self.assertTrue( dicts_equal( Oblk.elementary_errorgens, { ('S', 'X'): 0.2, ('S', 'Y'): 0.25, ('S', 'Z'): 0.0, ('C', 'X', 'Y'): 0.05, ('A', 'X', 'Y'): -0.01, ('C', 'X', 'Z'): 0, ('A', 'X', 'Z'): 0, ('C', 'Y', 'Z'): 0, ('A', 'Y', 'Z'): 0, }))
def run_process_tomography(state_to_density_matrix_fn, n_qubits=1, comm=None, verbose=False, basis='pp', time_dependent=False, opt_args={}): """ A function to compute the process matrix for a quantum channel given a function that maps a pure input state to an output density matrix. Args: state_to_density_matrix_fn : (function: array -> array) The function that computes the output density matrix from an input pure state. n_qubits : (int, optional, default 1) The number of qubits expected by the function. Defaults to 1. comm : (MPI.comm object, optional) An MPI communicator object for parallel computation. Defaults to local comm. verbose : (bool, optional, default False) How much detail to send to stdout basis : (str, optional, default 'pp') The basis in which to return the process matrix time_dependent : (bool, optional, default False ) If the process is time dependent, then expect the density matrix function to return a list of density matrices, one at each time point. opt_args : (dict, optional) Optional keyword arguments for state_to_density_matrix_fn Returns: numpy.ndarray The process matrix representation of the quantum channel in the basis specified by 'basis'. If 'time_dependent'=True, then this will be an array of process matrices. """ if comm is not None: rank = comm.Get_rank() size = comm.Get_size() else: rank = 0 size = 1 if verbose: print('Running process tomography as %d of %d on %s.' % (comm.Get_rank(), comm.Get_size(), comm.Get_name())) # Define and preprocess the input test states one_qubit_states = _np.array([[1, 0], [0, 1], [1, 1], [1., 1.j]], dtype='complex') one_qubit_states = [state / _lin.norm(state) for state in one_qubit_states] states = _itertools.product(one_qubit_states, repeat=n_qubits) states = [multi_kron(*state) for state in states] in_density_matrices = [_np.outer(state, state.conj()) for state in states] in_states = _np.column_stack( list([vec(rho) for rho in in_density_matrices])) my_states = split(size, states)[rank] if verbose: print("Process %d of %d evaluating %d input states." % (rank, size, len(my_states))) if time_dependent: my_out_density_matrices = [ state_to_density_matrix_fn(state, **opt_args) for state in my_states ] else: my_out_density_matrices = [[ state_to_density_matrix_fn(state, **opt_args) ] for state in my_states] # Assemble the outputs if comm is not None: gathered_out_density_matrices = comm.gather(my_out_density_matrices, root=0) else: gathered_out_density_matrices = [my_out_density_matrices] if rank == 0: # Postprocess the output states to compute the process matrix # Flatten over processors out_density_matrices = _np.array( [y for x in gathered_out_density_matrices for y in x]) # Sort the list by time out_density_matrices = _np.transpose(out_density_matrices, [1, 0, 2, 3]) out_states = [ _np.column_stack( list([vec(rho) for rho in density_matrices_at_time])) for density_matrices_at_time in out_density_matrices ] process_matrices = [ _np.dot(out_states_at_time, _lin.inv(in_states)) for out_states_at_time in out_states ] process_matrices = [ change_basis(process_matrix_at_time, 'col', basis) for process_matrix_at_time in process_matrices ] if not time_dependent: return process_matrices[0] else: return process_matrices else: # print(f'Rank {rank} returning NONE from comm {comm}.') return None
def jamiolkowski_iso(operation_mx, op_mx_basis='pp', choi_mx_basis='pp'): """ Given a operation matrix, return the corresponding Choi matrix that is normalized to have trace == 1. Parameters ---------- operation_mx : numpy array the operation matrix to compute Choi matrix of. op_mx_basis : Basis object The source and destination basis, respectively. Allowed values are Matrix-unit (std), Gell-Mann (gm), Pauli-product (pp), and Qutrit (qt) (or a custom basis object). choi_mx_basis : Basis object The source and destination basis, respectively. Allowed values are Matrix-unit (std), Gell-Mann (gm), Pauli-product (pp), and Qutrit (qt) (or a custom basis object). Returns ------- numpy array the Choi matrix, normalized to have trace == 1, in the desired basis. """ operation_mx = _np.asarray(operation_mx) op_mx_basis = _bt.create_basis_for_matrix(operation_mx, op_mx_basis) opMxInStdBasis = _bt.change_basis(operation_mx, op_mx_basis, op_mx_basis.create_equivalent('std')) #expand operation matrix so it acts on entire space of dmDim x dmDim density matrices # so that we can take dot products with the BVec matrices below opMxInStdBasis = _bt.resize_std_mx(opMxInStdBasis, 'expand', op_mx_basis.create_equivalent( 'std'), op_mx_basis.create_simple_equivalent('std')) N = opMxInStdBasis.shape[0] # dimension of the full-basis (expanded) gate dmDim = int(round(_np.sqrt(N))) # density matrix dimension #Note: we need to use the *full* basis of Matrix Unit, Gell-Mann, or Pauli-product matrices when # generating the Choi matrix, even when the original operation matrix doesn't include the entire basis. # This is because even when the original operation matrix doesn't include a certain basis element (B0 say), # conjugating with this basis element and tracing, i.e. trace(B0^dag * Operation * B0), is not necessarily zero. #get full list of basis matrices (in std basis) -- i.e. we use dmDim if not isinstance(choi_mx_basis, _Basis): choi_mx_basis = _Basis.cast(choi_mx_basis, N) # we'd like a basis of dimension N BVec = choi_mx_basis.create_simple_equivalent().elements M = len(BVec) # can be < N if basis has multiple block dims assert(M == N), 'Expected {}, got {}'.format(M, N) choiMx = _np.empty((N, N), 'complex') for i in range(M): for j in range(M): BiBj = _np.kron(BVec[i], _np.conjugate(BVec[j])) BiBj_dag = _np.transpose(_np.conjugate(BiBj)) choiMx[i, j] = _mt.trace(_np.dot(opMxInStdBasis, BiBj_dag)) \ / _mt.trace(_np.dot(BiBj, BiBj_dag)) # This construction results in a Jmx with trace == dim(H) = sqrt(operation_mx.shape[0]) # (dimension of density matrix) but we'd like a Jmx with trace == 1, so normalize: choiMx_normalized = choiMx / dmDim return choiMx_normalized