def bell_gen(N=2): """ Generates the matrix product state of an N qubit Bell state |psi^+>. Nice bit of code. """ # first generate an N qubit state in |0>^\otimes N mps = mp.MPArray.from_kron([np.array([1, 0])] * N) # generate entanglement operator in MPO form hadamard_mpo = mporep(mpo_dict["h"], mpo_dict["id"], reps=N - 1) cx_mpo = mp.chain([mpo_dict["id"]] * N) for i in range(0, N - 1): # construct selector for iteration selector = [0] * (N - 1) selector[i] = 1 # construct CX stage cx_mpo_stage = mpojob([mpo_dict["id"], mpo_dict["cx"]], selector) # add to cx sequence cx_mpo = mp.dot(cx_mpo_stage, cx_mpo) # define entangling operation in MPO form entangle_op = mp.dot(cx_mpo, hadamard_mpo) # compress to minimise memory overhead (TODO: must add overlap check) entangle_op.compress("svd", relerr=1e-6) # compute Bell state in MPS form bell = mp.dot(entangle_op, mps) return bell
def _get_left_sweep(self, trotter_exponentials, post_swap, pre_swap): """ Builds a list of tuples, which contain all the operators, that are necessary for the complete sweep from the system site, to the left edge of the chain and back. Sweeping to the left, we have: an evolution operator, followed by a post_swap Sweeping back to the right we have: a pre-swap, followed by an evolution operator Both combined to a single mpo. Each entry in the list contains a tuple, the first element of the tuple is the index of the left one of the two sites in the chain, for which the above mentioned operator applies. The second element is the operator itself :param trotter_exponentials: List of trotterized unitary operators :param post_swap: List of swap operators to be applied after the time-evo operators :param pre_swap: List of swap operators to be applied before the time-evo operators :return: List of tuples with entries as described above """ # System site is back at the start left_sweep = [] # sweep left for site in range(self.system_index - 1, 0, -1): left_sweep.append( (site, self._compress_mpo( mp.dot(post_swap[site], trotter_exponentials[site]).group_sites(2)))) # left edge propagataion left_sweep.append((0, trotter_exponentials[0].group_sites(2))) # sweep back to the start for site in range(1, self.system_index): left_sweep.append((site, self._compress_mpo( mp.dot(trotter_exponentials[site], pre_swap[site]).group_sites(2)))) return left_sweep
def fast_evolve(self, end=True): """ Perform a Trotterized time evolution step by tau. Compress after each dot product. Renormalizes state after one full Trotter-step due to compression. Without setting the end flag, the evolution does not do a complete trotter step, and the resulting state is therefore not immediately useable. Not setting the end flag allows for leaving out some unnecessary double steps and can therefore be used for propagating intermediate steps :parameter end: Flag, which can be set to false, if the current propagation only needs to produce an intermediate step, which is not used otherwise. :return: """ self.last_state_norm = self.curr_state_norm if self.start and end: trotter_steps = self.propagator.trotter_steps elif self.start and not end: trotter_steps = self.propagator.start_trotter_steps self.start = False elif not self.start and end: trotter_steps = self.propagator.end_trotter_steps self.start = True else: trotter_steps = self.propagator.step_trotter_steps if not self.canonicalize_every_step: canonicalize_to(self.psi_t, to_cform=self.to_cform) for index, (trotter_ui, trotter_ui_adj) in enumerate(trotter_steps): self.psi_t = mp.dot(trotter_ui, self.psi_t, self.axes) if self.canonicalize_every_step: self.psi_t.compress(**self.state_compression_kwargs) else: if index < self.len_trotter_steps - 1: self.psi_t.compress(**self.state_compression_kwargs_noc) else: if self.canonicalize_last_step: self.psi_t.compress(**self.state_compression_kwargs) else: self.psi_t.compress( **self.state_compression_kwargs_noc) self.psi_t = mp.dot(self.psi_t, trotter_ui.adj(), self.axes) if self.canonicalize_every_step: self.psi_t.compress(**self.state_compression_kwargs) else: if index < self.len_trotter_steps - 1: self.psi_t.compress(**self.state_compression_kwargs_noc) else: if self.canonicalize_last_step: self.psi_t.compress(**self.state_compression_kwargs) else: self.psi_t.compress( **self.state_compression_kwargs_noc) self._normalize_state() self.curr_state_norm = mp.norm(self.psi_t) self.trotter_error += 2 * self.propagator.step_trotter_error self.stepno += 1 if self.start and not end: self.start = False elif not self.start and end: self.start = True
def one_site_mean(self, site, a): #making one point observable in the form of mpo left = [mp.MPArray.from_kron([idm] * (site - 1))] if (site - 1) > 0 else [] right = [mp.MPArray.from_kron([idm] * (self.sites - site))] \ if (self.sites - site) > 0 else [] mpo = mp.chain(left + [mp.MPArray.from_array_global(a)] + right) #returning matrix element return mp.dot(mp.dot(self.psi.conj(), mpo), self.psi).to_array()
def corr(self, site1, site2, a, b): #making two points observable in the form of mpo left = [mp.MPArray.from_kron([idm] * (site1 - 1))] if (site1 - 1) > 0 else [] right = [mp.MPArray.from_kron([idm] * (self.sites - site2))] \ if (self.sites - site2) > 0 else [] mid = [mp.MPArray.from_kron([idm] * (site2 - site1 - 1))] \ if (site2 - site1 - 1) > 0 else [] mpo = mp.chain(left + [mp.MPArray.from_array_global(a)] + mid + \ [mp.MPArray.from_array_global(b)] + right) if site1 != site2 else\ mp.chain(left + [mp.MPArray.from_array_global(a.dot(b))]\ + right) #returning matrix element return mp.dot(mp.dot(self.psi.conj(), mpo), self.psi).to_array()
def operator_find(gen, N, relerr=1e-6): """ Computes all unique stabilisers given the list from gen. Assumes Pauli operators. """ # stabiliser storage stabilisers = [] # max depth of stabiliser max_depth = len(gen) + 1 # list of generator identifiers indices = range(0, len(gen)) # iterate over length N stabiliser for i in range(1, max_depth): # generate every unique generator sequence - assumes Pauli based generators gen_combs = set(list(combinations(indices, i))) # iterate over index combinations for comb in gen_combs: # create new stab = mpojob([mpo_dict["id"]], [0] * N) for k in comb: # add generator to stabiliser sequence stab = mp.dot(gen[k], stab) # add new stabiliser to set stabilisers.append(stab) return stabilisers
def evolve(self): """ Perform a Trotterized time evolution step by tau. Renormalizes state after one full Trotter-step due to compression. :return: """ overlap = self.base_overlap if not self.canonicalize_every_step: canonicalize_to(self.psi_t, to_cform=self.to_cform) for index, trotter_ui in enumerate(self.propagator.trotter_steps): self.psi_t = mp.dot(trotter_ui, self.psi_t, self.axes) if self.canonicalize_every_step: overlap *= self.psi_t.compress(**self.state_compression_kwargs) else: if index < self.len_trotter_steps-1: overlap *= self.psi_t.compress(**self.state_compression_kwargs_noc) else: if self.canonicalize_last_step: overlap *= self.psi_t.compress(**self.state_compression_kwargs) else: overlap *= self.psi_t.compress(**self.state_compression_kwargs_noc) self.pmps_compression_step += 1 if self.pmps_compression_step == self.compress_sites_step: compress_pmps_sites(self.psi_t, relerr=self.compress_sites_relerr, rank=self.compress_sites_rank, stable=self.compress_sites_stable, to_cform=self.to_cform) self.pmps_compression_step = 0 self._normalize_state() self.cumulative_overlap *= overlap self.last_overlap = overlap self.trotter_error += self.propagator.step_trotter_error self.stepno += 1
def bures_mps(rho, sigma): """ Computes the Bures angle for two matrix product states. Requires rho and sigma to be pure and input as MPOs """ fid = mp.trace(mp.dot(rho, sigma)) return np.arccos(np.clip(np.sqrt(fid), 0.0, 1.0))
def test_povm_ic_mpa(nr_sites, local_dim, rank, rgen): # Check that the tensor product of the PauliGen POVM is IC. paulis = povm.pauli_povm(local_dim) inv_map = mp_from_array_repeat(paulis.linear_inversion_map, nr_sites) probab_map = mp_from_array_repeat(paulis.probability_map, nr_sites) reconstruction_map = mp.dot(inv_map, probab_map) eye = factory.eye(nr_sites, local_dim**2) assert mp.norm(reconstruction_map - eye) < 1e-5 # Check linear inversion for a particular example MPA. # Linear inversion works for arbitrary matrices, not only for states, # so we test it for an arbitrary MPA. # Normalize, otherwise the absolute error check below will not work. mpa = factory.random_mpa(nr_sites, local_dim**2, rank, dtype=np.complex_, randstate=rgen, normalized=True) probabs = mp.dot(probab_map, mpa) recons = mp.dot(inv_map, probabs) assert mp.norm(recons - mpa) < 1e-6
def stepsize(A, g, X): X.normalize(left=len(X) - 1) # get the left-eigenvectors Us, _ = X.split(len(X) - 2) Us = Us.reshape([s + (1, ) for s in Us.pdims[:-1]] + [Us.pdims[-1]]) proj = mp.dot(Us.conj(), Us, axes=(1, 1)) g = mp.partialdot(proj, g, start_at=0) g.compress(**compargs) return stepfun(A, g, _)
def sandwich(op, propagator, startsite=0): """ Calculates the expectation value of an operator in mpo form with a reduced state: <psi_t_red| mpo |psi_t_red>. nof_sites can be inferred from len(mpo) here :param op: Operator for which to take expectation value as mpo :param propagator: TMPSPropagator object. The reduced state |psi_red> is generated from propagator.psi_t :param startsite: first site of the reduced state (may be negative, indexing works like for python lists) :return: expectation value <psi_t_red| mpo |psi_t_red> """ reduced = reduction(propagator, startsite=startsite, nof_sites=len(op)) # TODO: maybe compress first after dot? return mp.trace(mp.dot(op, reduced))
def evolve(self): """ Perform a Trotterized time evolution step by tau. Compress after each dot product. Renormalizes state after one full timestep due to compression. :return: """ self.last_state_norm = self.curr_state_norm # Can use the symmetry of the Suzuki-Trotter decomposition here to do it in one loop and # the fact, that for hermitian hi we have U = e^(-i*tau*hi), U^dag = U* for index, (trotter_ui, trotter_ui_adj) in enumerate( self.propagator.trotter_steps): self.psi_t = mp.dot(trotter_ui, self.psi_t, self.axes) if self.canonicalize_every_step: self.psi_t.compress(**self.state_compression_kwargs) else: if index < self.len_trotter_steps - 1: self.psi_t.compress(**self.state_compression_kwargs_noc) else: if self.canonicalize_last_step: self.psi_t.compress(**self.state_compression_kwargs) else: self.psi_t.compress( **self.state_compression_kwargs_noc) self.psi_t = mp.dot(self.psi_t, trotter_ui_adj, self.axes) if self.canonicalize_every_step: self.psi_t.compress(**self.state_compression_kwargs) else: if index < self.len_trotter_steps - 1: self.psi_t.compress(**self.state_compression_kwargs_noc) else: if self.canonicalize_last_step: self.psi_t.compress(**self.state_compression_kwargs) else: self.psi_t.compress( **self.state_compression_kwargs_noc) self._normalize_state() self.curr_state_norm = mp.norm(self.psi_t) self.trotter_error += 2 * self.propagator.step_trotter_error self.stepno += 1
def sandwich_mpa(op, psi, mpa_type): """ Calculates <op>_{psi} = tr(op*psi) for a state of specified mpa_type """ if mpa_type == 'mps': psi = mp.mps_to_mpo(psi) elif mpa_type == 'pmps': psi = mp.pmps_to_mpo(psi) elif mpa_type == 'mpo': pass else: raise AssertionError('Unknown mpa_type') return mp.trace(mp.dot(op, psi))
def sandwich_state(op, psi, mpa_type, startsite=0): """ Calculates the expectation value of an operator in mpo form with a reduced state: <psi_red| mpo |psi_red>. nof_sites can be inferred from len(mpo) here :param op: Operator for which to take expectation value as mpo :param psi: State from which to generate reduced state |psi_red> :param startsite: first site of the reduced state :param mpa_type: mpa type of psi (mps, pmo or pmps) :return: expectation value <psi_red| mpo |psi_red> """ reduced = state_reduction(psi, mpa_type, startsite=startsite, nof_sites=len(op)) # TODO: maybe compress first after dot? return mp.trace(mp.dot(op, reduced))
def angle_estimate(stabilisers, state, N, shots=500): """ Compute Bures angle given the perturbed state and the stabilisers for the True state """ # initialise total stab_sum = 0.0 # iterate over stabilisers for stab in stabilisers: # compute out probability given perturbed state prob = (1 + mp.trace(mp.dot(stab, state))) / 2 stab_sum += mle(rand_res([1 - prob, prob], num=int(shots))) theta = theta_compute(stab_sum, N=N) # TODO: Fix this uncertainty calculation issue uncert = 0 # np.sqrt(var(theta, stab_sum, int(N))) return theta, uncert
def calculate_expectation_values(states, observable): """ Calculates the expectation values :math:`\\langle M \\rangle_i` of the the ``observable`` :math:`M` with respect to the ``states`` :math:`\\{\\rho\\}` .. math:: \\langle M \\rangle_i = \\text{tr}(\\rho_i M) For this function to work, the observable has to have the same dimension as the density matrix which represents the state, i.e. all states must have the same dimensions. This function is meant to work with a list of evolved states of the same system at different times. Args: states (list[mpnum.MPArray]): List of states :math:`\\{\\rho\\}`. They are assumed to be MPOs and already normalized observable (numpy.ndarray): The matrix representing the observable :math:`M` in global form (as opposed to local form. Global form is just the usual form to write matrices in QM. For more information, see the ``mpnum`` documentation.) Returns: list[mpnum.MPArray.dtype]: List of expectation values for the states """ if not all(state.shape == states[0].shape for state in states): raise ValueError("The states in the provided list are not all of the " "same shape.") if len(observable) != np.prod(np.array([_[0] for _ in states[0].shape])): raise ValueError("Observable dimensions and state dimensions do not " "fit") expct_values = [ mp.trace( mp.dot( state, tmps.matrix_to_mpo(observable, [[_[0]] * 2 for _ in state.shape]))) for state in states ] return expct_values
def _time_evolution(state, us, step_numbers, subsystems, tau, method, trotter_compr, v): """ Implements time-evolution via Trotter-Suzuki decomposition Args: state (mpnum.MPArray): The state to be evolved in time us (list[mpnum.MPArray]): List of ordered operator exponentials for a single Trotter slice step_numbers (list[int]): List of time steps as generated by :func:`_times_to_steps` subsystems (list[list[int]]): Sites for which the subsystem states should be returned at the respective times tau (float): Duration of one Trotter slice. As defined in :func:`_times_to_steps` method (str): Which method to use. Either 'mps', 'mpo' or 'pmps'. trotter_compr (dict): Compression parameters used in the iterations of Trotter-Suzuki decomposition. v (int): Level of verbose output. 0 means no output, 1 means that some basic output showing the progress of calculations is produced. 2 will in addition show the bond dimensions of the state after every couple of iterations, 3 will show bond dimensions after every Trotter iteration. Returns: list[list[float], list[list[int]], list[mpnum.MPArray]: A list with five items: (i) The list of times for which the density matrices have been computed (ii) The list indicating which subsystems of the system are returned at the respective time of the first list (iii) The list of density matrices as MPO or PMPS as mpnum.MPArray, depending on the input "method". If that was MPS, the full states will still be MPSs, the reduced ones will be MPOs. """ c = Counter(step_numbers) times = [] states = [] compr_errors = [] trot_errors = [] var_compression = False if trotter_compr['method'] == 'var': var_compression = True accumulated_overlap = 1 accumulated_trotter_error = 0 for i in range(max(step_numbers) + 1): for j in range(c[i]): _append(times, states, compr_errors, trot_errors, tau, i, j, step_numbers, subsystems, state, accumulated_overlap, accumulated_trotter_error, method) for u in us: if var_compression: trotter_compr['startmpa'] = mp.MPArray.copy(state) state = mp.dot(u, state) accumulated_overlap *= state.compress(**trotter_compr) if method == 'mpo': for u in us: if var_compression: trotter_compr['startmpa'] = mp.MPArray.copy(state) state = mp.dot(state, u.T.conj()) accumulated_overlap *= state.compress(**trotter_compr) state = normalize(state, method) accumulated_trotter_error += tau**3 if (v == 1 or v == 2) and np.sqrt(i + 1) % 1 == 0 and i < \ step_numbers[-1]: print(str(i + 1) + " Trotter iterations finished...") if v == 2: print("Ranks: " + str(state.ranks)) if v == 3 and i < step_numbers[-1]: print(str(i + 1) + " Trotter iterations finished...") print("Ranks: " + str(state.ranks)) if v != 0: print("Done with time evolution") return times, subsystems, states # , compr_errors, trot_errors
def _time_evolution(state, us, ts, subsystems, tau, method, trotter_compr, v): """ Implements time-evolution via Trotter-Suzuki decomposition Args: state (mpnum.MPArray): The state to be evolved in time us (list[mpnum.MPArray]): List of ordered operator exponentials for a single Trotter slice ts (list[int]): List of time steps as generated by _times_to_steps() subsystems (list[list[int]]): Sites for which the subsystem states should be returned at the respective times tau (float): Duration of one Trotter slice. As defined in _times_to_steps() method (str): Which method to use. Either 'mps', 'mpo' or 'pmps'. trotter_compr (dict): Compression parameters used in the iterations of Trotter-Suzuki decomposition. v (bool): Verbose (Yes) or not verbose (No). Will print what is going on vs. will not print anything. Returns: list[list[float], list[list[int]], list[mpnum.MPArray], list[float], list[float]]: A list with five items: (i)The list of times for which the density matrices have been computed (ii) The list indicating which subsystems of the system are returned at the respective time of the first list (iii) The list of density matrices as MPO or PMPS as mpnum.MPArray, depending on the input "method". If that was MPS, the full states will still be MPSs, the reduced ones will be MPOs. (iv) The errors due to compression during the procedure (v) The order of errors due to application of Trotter during the procedure """ # NOTE: is this for debugging purposes? c = Counter(ts) times = [] states = [] compr_errors = [] trot_errors = [] var_compression = False if trotter_compr['method'] == 'var': var_compression = True accumulated_overlap = 1 accumulated_trotter_error = 0 for i in range(ts[-1] + 1): for j in range(c[i]): _append(times, states, compr_errors, trot_errors, tau, i, j, ts, subsystems, state, accumulated_overlap, accumulated_trotter_error, method) for u in us: if var_compression: trotter_compr['startmpa'] = mp.MPArray.copy(state) state = mp.dot(u, state) accumulated_overlap *= state.compress(**trotter_compr) if method == 'mpo': for u in us: if var_compression: trotter_compr['startmpa'] = mp.MPArray.copy(state) state = mp.dot(state, u.T.conj()) accumulated_overlap *= state.compress(**trotter_compr) state = normalize(state, method) accumulated_trotter_error += tau**3 if v and np.sqrt(i) % 1 == 0 and i != 0: print(str(i) + " Trotter iterations finished...") if v: print("Done with time evolution") return times, subsystems, states, compr_errors, trot_errors
def UniU_error(U, N, perturbations=100, exact=True, shots=8000): """ Check estimation error for an input U, number of perturbations and wether to use exact or finite sampling. """ # generate entangled states rho = bell_gen(N=N) # generate Bell state stabilisers bell_gstab = bell_stab_gen(N=N) # convert to MPO if required if type(U) != mp.mparray.MPArray: try: U = mp.MPArray.from_array_global(U.reshape([2] * N * 2), ndims=2) except ValueError: # catch operator dimension mismatch raise ValueError( "Cannot reshape unitary into MPO, check dimensions") # apply to entangled state rho = mp.dot(U, rho) # evolve generators under unitary gstab = [mp.dot(mp.dot(U, stb), U.adj()) for stb in bell_gstab] # generate stabiliser set stabilisers = operator_find(gstab, N=N) # apply to entangled state and convert to MPO for measurement phase rho = mp.mpsmpo.mps_to_mpo(rho) # calculate the estimation error for requested number of perturbations error = [] # initialise random unitary generator U_perturb = random_MPUgen(N) for i in range(0, perturbations): print("Now computing unitary perturbation {}\r".format(i), end="", flush=True) # make a copy rho_c = rho.copy() # compute a local perturbation using generator U_p = next(U_perturb) # apply to Choi state rho_c = mp.dot(mp.dot(U_p, rho_c), U_p.adj()) # compute expectation values exactly or with finite samples if exact: Q = 0.0 # iterate over stabiliser measurements for stab_proj in stabilisers: # add to Q sum Q += (1 + mp.trace(mp.dot(stab_proj, rho_c))) / 2 print(Q) # estimate angle a_est = theta_compute(Q, N=N) else: # estimate expectation values from finite number of outcomes a_est, a_uncert = angle_estimate(stabilisers, rho_c, N=N, shots=shots) if np.abs(np.real(bures_mps(rho, rho_c) - a_est)) > 0.5: print( "High estimation error: {:.3f}, something has gone wrong". format(a_est)) continue # compute angle estimate error error.append(np.real(bures_mps(rho, rho_c) - a_est)) if exact: # output average estimation error - should always be small (<1e-4 depending on MPO compression) print("Average estimation error for {} perturbations: {:.3f}".format( perturbations, np.real(np.mean(error)))) else: # plot errors as histogram n, bins, patches = plt.hist(x=error, bins=len(error) // 10, alpha=0.65, color='red', histtype='step') plt.xlabel("Error") plt.ylabel("Counts") plt.title("Error distribution for {} qubit Clifford+T unitary".format( N // 2)) plt.show()