def run_trotterized_solver(self, state: State, t0, tf, num=50, schedule=lambda t: None, times=None, full_output=True, verbose=False): """Trotterized approximation of the Schrodinger equation""" assert state.is_ket # s is a ket specifying the initial codes # tf is the total simulation time if times is None: times = np.linspace(t0, tf, num=num) n = len(times) if full_output: z = np.zeros((n, state.shape[0], state.shape[1]), dtype=np.complex128) infodict = {'t': times} s = state.copy() for (i, t) in zip(range(n), times): schedule(t) if t == times[0] and full_output: z[i, ...] = state else: dt = times[i]-times[i-1] for hamiltonian in self.hamiltonians: s = hamiltonian.evolve(s, dt) if full_output: z[i, ...] = s else: z = np.array([s]) norms = np.linalg.norm(z, axis=(-2, -1)) if verbose: print('Fraction of integrator results normalized:', len(np.argwhere(np.isclose(norms, np.ones(norms.shape)) == 1)) / len(norms)) print('Final state norm - 1:', norms[-1] - 1) norms = norms[:, np.newaxis, np.newaxis] z = z / norms return z, infodict
def run_trotterized_solver(self, state: State, t0, tf, num=50, schedule=lambda t: None, times=None, full_output=True, verbose=False): """Trotterized approximation of the Schrodinger equation""" assert not state.is_ket # s is a ket specifying the initial codes # tf is the total simulation time if times is None: times = np.linspace(0, 1, num=int(num)) * (tf - t0) + t0 n = len(times) if full_output: z = np.zeros((n, state.shape[0], state.shape[1]), dtype=np.complex128) infodict = {'t': times} s = state.copy() for (i, t) in zip(range(n), times): schedule(t) if t == times[0] and full_output: z[i, ...] = state else: if i != 0: dt = times[i] - times[i - 1] else: dt = times[i + 1] - times[i] for hamiltonian in self.hamiltonians: s = hamiltonian.evolve(s, dt) for jump_operator in self.jump_operators: if isinstance(jump_operator, LindbladJumpOperator): if i == 1: print( 'Warning: Evolving by a LindbladJumpOperator involves exponentiating a large matrix.', 'Consider a QuantumChannel.') s = jump_operator.evolve(s, dt) elif isinstance(jump_operator, QuantumChannel): s = jump_operator.evolve(s, dt) if full_output: z[i, ...] = s if not full_output: z = np.array([s]) norms = np.trace(z, axis1=-2, axis2=-1) if verbose: print( 'Fraction of integrator results normalized:', len(np.argwhere(np.isclose(norms, np.ones(norms.shape)) == 1)) / len(norms)) print('Final state norm - 1:', norms[-1] - 1) norms = norms[:, np.newaxis, np.newaxis] z = z / norms return z, infodict
def test_single_qubit_operation(self): N = 6 # Test single qubit operation psi0 = State(np.zeros((rydberg.d**N, 1)), code=rydberg) psi0[0][0] = 1 # Apply sigma_y on the second qubit to get 1j|020000> psi0 = rydberg.multiply(psi0, [1], rydberg.Y) self.assertTrue(psi0[(rydberg.d - 1) * rydberg.d**(N - 2), 0] == 1j) # Apply sigma_z on the second qubit, codes is -1j|010000> psi0 = rydberg.multiply(psi0, [1], rydberg.Z) self.assertTrue(psi0[(rydberg.d - 1) * rydberg.d**(N - 2), 0] == -1j) # Apply sigma_x on qubits psi0 = rydberg.multiply(psi0, [0, 2, 3, 4, 5], tools.tensor_product([rydberg.X] * (N - 1))) # Vector is still normalized self.assertTrue(np.vdot(psi0, psi0) == 1) # Should be -1j|111111> self.assertTrue(psi0[-1, 0] == -1j) # Test rydberg operations with density matrices psi1 = np.array([ tools.tensor_product([np.array([1, 0, 1]), np.array([1, 0, 0])]) / 2**(1 / 2) ]).T psi1 = State(tools.outer_product(psi1, psi1), code=rydberg) # Apply sigma_Y to first qubit psi2 = np.array([np.kron([1, 0, -1], [1, 0, 0]) * -1j / 2**(1 / 2)]).T rho2 = tools.outer_product(psi2, psi2) psi1 = rydberg.multiply(psi1, [0], ['Y']) self.assertTrue(np.linalg.norm(psi1 - rho2) <= 1e-10) psi0 = np.array([ tools.tensor_product([np.array([1, 0, 1]), np.array([1, 0, 0])]) / 2**(1 / 2) ]).T psi0 = State(tools.outer_product(psi0, psi0), code=rydberg) psi1 = State(psi0.copy(), code=rydberg) # Apply sigma_Y to first qubit psi2 = np.array([np.kron([1, 0, -1], [1, 0, 0]) * -1j / 2**(1 / 2)]).T psi2 = tools.outer_product(psi2, psi2) # Apply single qubit operation to dmatrix psi0 = rydberg.multiply(psi0, [0], rydberg.Y) self.assertTrue(np.linalg.norm(psi0 - psi2) <= 1e-10) # Test on ket psi1 = rydberg.multiply(psi1, [0], rydberg.Y) self.assertTrue(np.linalg.norm(psi1 - psi2) <= 1e-10)
def test_multi_qubit(self): N = 5 psi0 = State( tools.tensor_product([two_qubit_code.logical_basis[0]] * N)) psi1 = psi0.copy() op = tools.tensor_product( [two_qubit_code.X, two_qubit_code.Y, two_qubit_code.Z]) psi0 = two_qubit_code.multiply(psi0, [1, 3, 4], op) psi1 = two_qubit_code.multiply(psi1, [1, 3, 4], ['X', 'Y', 'Z']) self.assertTrue(np.allclose(psi0, psi1))
def test_multi_qubit(self): n = 5 psi0 = State( tools.tensor_product([jordan_farhi_shor.logical_basis[0]] * n)) psi1 = psi0.copy() op = tools.tensor_product( [jordan_farhi_shor.X, jordan_farhi_shor.Y, jordan_farhi_shor.Z]) psi0 = jordan_farhi_shor.multiply(psi0, [1, 3, 4], op) psi1 = jordan_farhi_shor.multiply(psi1, [1, 3, 4], ['X', 'Y', 'Z']) self.assertTrue(np.allclose(psi0, psi1))
def test_multi_qubit_operation(self): N = 6 psi0 = State(np.zeros((rydberg.d**N, 1)), code=rydberg) psi0[0, 0] = 1 psi1 = psi0.copy() op = tools.tensor_product([rydberg.X, rydberg.Y, rydberg.Z]) psi0 = rydberg.multiply(psi0, [1, 3, 4], op) psi1 = rydberg.multiply(psi1, [1], 'X') psi1 = rydberg.multiply(psi1, [3], 'Y') psi1 = rydberg.multiply(psi1, [4], 'Z') self.assertTrue(np.allclose(psi0, psi1))
def test_multi_qubit_pauli(self): N = 6 psi0 = State(np.zeros((rydberg.d**N, 1)), code=rydberg) psi0[0, 0] = 1 psi1 = psi0.copy() psi2 = psi0.copy() psi3 = psi0.copy() psi4 = psi0.copy() psi0 = rydberg.multiply(psi0, [1, 3, 4], ['X', 'Y', 'Z']) psi1 = rydberg.multiply(psi1, [1], 'X') psi1 = rydberg.multiply(psi1, [3], 'Y') psi1 = rydberg.multiply(psi1, [4], 'Z') psi2 = rydberg.multiply(psi2, [4, 1, 3], ['Z', 'X', 'Y']) psi3 = rydberg.multiply( psi3, [1, 3, 4], tools.tensor_product([rydberg.X, rydberg.Y, rydberg.Z])) psi4 = rydberg.multiply( psi4, [4, 1, 3], tools.tensor_product([rydberg.Z, rydberg.X, rydberg.Y])) self.assertTrue(np.allclose(psi0, psi1)) self.assertTrue(np.allclose(psi1, psi2)) self.assertTrue(np.allclose(psi2, psi3)) self.assertTrue(np.allclose(psi3, psi4))
def multiply(state: State, apply_to: Union[int, list], op): """ Apply a multi-qubit operator on several qubits (indexed in apply_to) of the input codes. :param state: input wavefunction or density matrix :type state: np.ndarray :param apply_to: zero-based indices of qudit locations to apply the operator :type apply_to: list of int :param op: Operator to act with. :type op: np.ndarray (2-dimensional) """ # Type handling if isinstance(apply_to, int): apply_to = [apply_to] if not isinstance(op, list): op = [op] pauli = False if isinstance(op, list): if all(isinstance(elem, str) for elem in op): pauli = True else: op = tensor_product(op) if not state.is_ket: if pauli: out = state.copy() for i in range(len(apply_to)): ind = d**apply_to[i] out = out.reshape( (-1, d, d**(state.number_physical_qudits - 1), d, ind), order='F') if op[i] == 'X': # Sigma_X out = np.flip(out, axis=(1, 3)) elif op[i] == 'Y': # Sigma_Y out = np.flip(out, axis=(1, 3)) out[:, d - 1, :, 0, :] = -out[:, d - 1, :, 0, :] out[:, 0, :, d - 1, :] = -out[:, 0, :, d - 1, :] elif op[i] == 'Z': # Sigma_Z out[:, d - 1, :, 0, :] = -out[:, d - 1, :, 0, :] out[:, 0, :, d - 1, :] = -out[:, 0, :, d - 1, :] out = out.reshape(state.shape, order='F') return State(out, is_ket=state.is_ket, IS_subspace=state.IS_subspace, code=state.code) else: # Note that the conjugate transpose it taken automatically in right_multiply return right_multiply(left_multiply(state, apply_to, op), apply_to, op) else: return left_multiply(state, apply_to, op)
def test_depolarize(self): # First test single qubit channel psi0 = State(np.zeros((4, 1))) psi0[0] = 1 psi0 = State(tools.outer_product(psi0, psi0)) psi1 = psi0.copy() psi2 = psi0.copy() psi3 = psi0.copy() psi4 = psi0.copy() p = 0.093 op0 = quantum_channels.DepolarizingChannel() psi0 = op0.channel(psi0, p, apply_to=1) op1 = quantum_channels.DepolarizingChannel() psi1 = op1.channel(psi1, 2 * p, apply_to=0) self.assertTrue(psi1[2, 2] == 0.124) self.assertTrue(psi0[1, 1] == 0.062) # Now test multi qubit channel psi2 = op0.channel(psi2, p, apply_to=[0, 1]) psi3 = op0.channel(psi3, p, apply_to=0) psi3 = op0.channel(psi3, p, apply_to=1) psi4 = op0.channel(psi4, p) self.assertTrue(np.allclose(psi2, psi3)) self.assertTrue(np.allclose(psi2, psi4)) expected = np.zeros((4, 4)) expected[0, 0] = 0.46827 expected[1, 1] = 0.03173 expected[2, 2] = 0.46827 expected[3, 3] = 0.03173 psi0 = op0.evolve(psi0, 20) self.assertTrue(np.allclose(expected, psi0)) psi0 = State(np.array([[1, 0], [0, 0]])) psi0 = op0.evolve(psi0, 20) self.assertTrue(np.allclose(psi0, .5 * np.identity(2)))
def multiply(state: State, apply_to: Union[int, list], op): """ Apply a multi-qubit operator on several qubits (indexed in apply_to) of the input codes. :param state: input wavefunction or density matrix :type state: np.ndarray :param apply_to: zero-based indices of qudit locations to apply the operator :type apply_to: list of int :param op: Operator to act with. :type op: np.ndarray (2-dimensional) """ if isinstance(apply_to, int): apply_to = [apply_to] if not isinstance(op, list): op = [op] # Type handling: to determine if Pauli, check if a list of strings pauli = False if isinstance(op, list): if all(isinstance(elem, str) for elem in op): pauli = True else: op = tools.tensor_product(op) if not state.is_ket: if pauli: out = state.copy() for i in range(len(apply_to)): # Note index start from the right (sN,...,s3,s2,s1) # Note index start from the right (sN,...,s3,s2,s1) if op[i] == 'X': # Sigma_X out = qubit.left_multiply( out, [n * apply_to[i], n * apply_to[i] + 2], ['Y', 'Y']) elif op[i] == 'Y': # Sigma_Y out = -1 * qubit.left_multiply( out, [n * apply_to[i] + 1, n * apply_to[i] + 2], ['X', 'X']) elif op[i] == 'Z': # Sigma_Z out = qubit.left_multiply( out, [n * apply_to[i], n * apply_to[i] + 1], ['Z', 'Z']) return State(out, is_ket=state.is_ket, IS_subspace=state.IS_subspace, code=state.code) else: return right_multiply(left_multiply(state, apply_to, op), apply_to, op) else: return left_multiply(state, apply_to, op)
def test_single_qubit(self): psi0 = State( tools.tensor_product([ three_qubit_code.logical_basis[0], three_qubit_code.logical_basis[0] ])) psi1 = psi0.copy() # Density matrix test psi2 = State(tools.outer_product(psi1, psi1)) psi0 = three_qubit_code.multiply(psi0, [1], ['Y']) # Test non-pauli operation psi1 = three_qubit_code.multiply(psi1, [1], three_qubit_code.Y) psi2 = three_qubit_code.multiply(psi2, [1], three_qubit_code.Y) res = 1j * tools.tensor_product([ three_qubit_code.logical_basis[0], three_qubit_code.logical_basis[1] ]) # Should get out 1j|0L>|1L> self.assertTrue(np.allclose(psi0, res)) self.assertTrue(np.allclose(psi1, res)) self.assertTrue(np.allclose(psi2, tools.outer_product(res, res))) self.assertTrue( np.allclose( three_qubit_code.multiply(psi0, [1], ['Z']), -1j * tools.tensor_product([ three_qubit_code.logical_basis[0], three_qubit_code.logical_basis[1] ]))) psi0 = three_qubit_code.multiply(psi0, [0], ['X']) # Should get out -1j|1L>|1L> self.assertTrue( np.allclose( psi0, 1j * tools.tensor_product([ three_qubit_code.logical_basis[1], three_qubit_code.logical_basis[1] ]))) # Rotate all qubits for i in range(2): psi0 = three_qubit_code.rotation(psi0, [i], np.pi / 2, three_qubit_code.X) self.assertTrue( np.allclose( psi0, -1j * tools.tensor_product([ three_qubit_code.logical_basis[0], three_qubit_code.logical_basis[0] ])))
def evolve(self, state: State, time, threshold=.05, apply_to: Union[int, list] = None): if state.is_ket: print('Converting ket to density matrix.') state = State(tools.outer_product(state, state)) if apply_to is None: apply_to = list(range(state.number_physical_qudits)) # Assume that apply_to is a list of integers if isinstance(apply_to, int): apply_to = [apply_to] n = 1 # Find a number of repetitions n small enough so that channel evolution is well approximated while (self.rates[0] * time)**2 / n > threshold: n += 1 p = self.rates[0] * time / n s = state.copy() # Apply channel n times for i in range(n): s = self.channel(s, p, apply_to=apply_to) return s
def variational_grad(self, param, initial_state=None): """Calculate the objective function F and its gradient exactly Input: param = parameters of QAOA Output: (F, Fgrad) F = <HamC> for minimization Fgrad = gradient of F with respect to param """ # TODO: make this work for continuous noise models if self.noise_model == 'continuous': raise NotImplementedError( 'Variational gradient does not currently support continuous noise model' ) param = np.asarray(param) # Preallocate space for storing copies of wavefunction - necessary for efficient computation of analytic # gradient if self.code.logical_code and initial_state is None: if isinstance(self.cost_hamiltonian, HamiltonianMIS): initial_state = State(tensor_product( [self.code.logical_basis[1]] * self.N), code=self.code) elif initial_state is None: if isinstance(self.cost_hamiltonian, HamiltonianMIS): initial_state = State(np.zeros( (self.cost_hamiltonian.hamiltonian.shape[0], 1)), code=self.code) initial_state[-1, -1] = 1 else: initial_state = State( np.ones((self.cost_hamiltonian.hamiltonian.shape[0], 1)) / np.sqrt(self.cost_hamiltonian.hamiltonian.shape[0]), code=self.code) if not (self.noise_model is None or self.noise_model == 'monte_carlo'): # Initial s should be a density matrix initial_state = State(outer_product(initial_state, initial_state), code=self.code) psi = initial_state if initial_state.is_ket: memo = np.zeros([psi.shape[0], 2 * self.depth + 2], dtype=np.complex128) memo[:, 0] = np.squeeze(psi.T) tester = psi.copy() else: memo = np.zeros([psi.shape[0], psi.shape[0], self.depth + 1], dtype=np.complex128) memo[..., 0] = np.squeeze(outer_product(psi, psi)) tester = State(outer_product(psi, psi), code=self.code) # Evolving forward for j in range(self.depth): if initial_state.is_ket: tester = self.hamiltonian[j].evolve(tester, param[j]) memo[:, j + 1] = np.squeeze(tester.T) else: self.hamiltonian[j].evolve(tester, param[j]) # Goes through memo, evolves every density matrix in it, and adds one more in the j*m+i+1 position # corresponding to H_i*p s0_prenoise = memo[..., 0] for k in range(j + 1): s = State(memo[..., k], code=self.code) s = self.hamiltonian[j].evolve(s, param[j]) if k == 0: s0_prenoise = s.copy() if self.noise_model is not None: if not (self.noise[j] is None): s = self.noise[j].evolve(s, param[j]) memo[..., k] = s.copy() s0_prenoise = self.hamiltonian[j].left_multiply(s0_prenoise) if self.noise_model is not None: if not (self.noise[j] is None): s0_prenoise = self.noise[j].evolve( s0_prenoise, param[j]) memo[..., j + 1] = s0_prenoise.copy() # Multiply by cost_hamiltonian if initial_state.is_ket: memo[:, self.depth + 1] = self.cost_hamiltonian.hamiltonian @ memo[:, self.depth] s = State(np.array([memo[:, self.depth + 1]]).T, code=self.code) else: for k in range(self.depth + 1): s = memo[..., k] s = State(self.cost_hamiltonian.hamiltonian * s, code=self.code) memo[..., k] = s # Evolving backwards, if ket: if initial_state.is_ket: for k in range(self.depth): s = self.hamiltonian[self.depth - k - 1].evolve( s, -1 * param[self.depth - k - 1]) memo[:, self.depth + k + 2] = np.squeeze(s.T) # Evaluating objective function if initial_state.is_ket: F = np.real(np.vdot(memo[:, self.depth], memo[:, self.depth + 1])) else: F = np.real(np.trace(memo[..., 0])) # Evaluating gradient analytically Fgrad = np.zeros(self.depth) for r in range(self.depth): if initial_state.is_ket: s = State(np.array([memo[:, 2 * self.depth + 1 - r]]).T, code=self.code) s = self.hamiltonian[r].left_multiply(s) Fgrad[r] = -2 * np.imag(np.vdot(memo[:, r], np.squeeze(s.T))) else: Fgrad[r] = 2 * np.imag(np.trace(memo[..., r + 1])) return F, Fgrad
def left_multiply(state: State, apply_to: Union[int, list], op): """ Apply a multi-qubit operator on several qubits (indexed in apply_to) of the input codes. :param state: input wavefunction or density matrix :type state: np.ndarray :param apply_to: zero-based indices of qudit locations to apply the operator :type apply_to: list of int :param op: Operator to act with. :type op: np.ndarray (2-dimensional) """ # Handle typing if isinstance(apply_to, int): apply_to = [apply_to] pauli = False if isinstance(op, list): if all(isinstance(elem, str) for elem in op): pauli = True else: op = tensor_product(op) n_op = len(apply_to) if not pauli: if is_sorted(apply_to): # Generate all shapes for left multiplication preshape = d * np.ones((2, n_op), dtype=int) preshape[1, 0] = int(state.dimension / (d**(1 + apply_to[n_op - 1]))) if n_op > 1: preshape[1, 1:] = np.flip(d**np.diff(apply_to)) / 2 shape1 = np.zeros(2 * n_op + 1, dtype=int) shape2 = np.zeros(2 * n_op + 1, dtype=int) order1 = np.zeros(2 * n_op + 1, dtype=int) order2 = np.zeros(2 * n_op + 1, dtype=int) shape1[:-1] = np.flip(preshape, axis=0).reshape((2 * n_op), order='F') shape1[-1] = -1 shape2[:-1] = preshape.reshape((-1), order='C') shape2[-1] = -1 preorder = np.arange(2 * n_op) order1[:-1] = np.flip(preorder.reshape((-1, 2), order='C'), axis=1).reshape((-1), order='F') order2[:-1] = np.flip(preorder.reshape((2, -1), order='C'), axis=0).reshape((-1), order='F') order1[-1] = 2 * n_op order2[-1] = 2 * n_op # Now left multiply out = state.reshape(shape1, order='F').transpose(order1) out = np.dot(op, out.reshape((d**n_op, -1), order='F')) out = out.reshape(shape2, order='F').transpose(order2) out = out.reshape(state.shape, order='F') return State(out, is_ket=state.is_ket, IS_subspace=state.IS_subspace, code=state.code) else: # Need to reshape the operator given apply_to = np.asarray(apply_to, dtype=int) new_shape = d * np.ones(2 * n_op, dtype=int) permut = np.argsort(apply_to) transpose_ord = np.zeros(2 * n_op, dtype=int) transpose_ord[:n_op] = (n_op - 1) * np.ones( n_op, dtype=int) - np.flip(permut, axis=0) transpose_ord[n_op:] = (2 * n_op - 1) * np.ones( n_op, dtype=int) - np.flip(permut, axis=0) sorted_op = np.reshape(np.transpose(np.reshape(op, new_shape, order='F'), axes=transpose_ord), (d**n_op, d**n_op), order='F') sorted_apply_to = apply_to[permut] return left_multiply(state, sorted_apply_to, sorted_op) else: # op should be a list of Pauli operators out = state.copy() for i in range(len(apply_to)): ind = d**apply_to[i] if state.is_ket: # Note index start from the right (sN,...,s3,s2,s1) out = out.reshape((-1, d, ind), order='F') if op[i] == 'X': # Sigma_X out = np.flip(out, 1) elif op[i] == 'Y': # Sigma_Y out = np.flip(out, 1) out[:, 0, :] = -1j * out[:, 0, :] out[:, d - 1, :] = 1j * out[:, d - 1, :] elif op[i] == 'Z': # Sigma_Z out[:, d - 1, :] = -out[:, d - 1, :] out = out.reshape(state.shape, order='F') else: out = out.reshape( (-1, d, d**(state.number_physical_qudits - 1), d, ind), order='F') if op[i] == 'X': # Sigma_X out = np.flip(out, 1) elif op[i] == 'Y': # Sigma_Y out = np.flip(out, axis=1) out[:, 0, :, :, :] = -1j * out[:, 0, :, :, :] out[:, d - 1, :, :, :] = 1j * out[:, d - 1, :, :, :] elif op[i] == 'Z': # Sigma_Z out[:, d - 1, :, :, :] = -out[:, d - 1, :, :, :] out = out.reshape(state.shape, order='F') return State(out, is_ket=state.is_ket, IS_subspace=state.IS_subspace, code=state.code)
def left_multiply(state: State, apply_to: Union[int, list], op): """ Apply a multi-qubit operator on several qubits (indexed in apply_to) of the input codes. :param state: input wavefunction or density matrix :type state: np.ndarray :param apply_to: zero-based indices of qudit locations to apply the operator :type apply_to: list of int :param op: Operator to act with. :type op: np.ndarray (2-dimensional) """ if isinstance(apply_to, int): apply_to = [apply_to] if not isinstance(op, list): op = [op] # Type handling: to determine if Pauli, check if a list of strings pauli = False if isinstance(op, list): if all(isinstance(elem, str) for elem in op): pauli = True else: op = tools.tensor_product(op) n_op = len(apply_to) if not pauli: if tools.is_sorted(apply_to): # Generate all shapes for left multiplication preshape = (d**n) * np.ones((2, n_op), dtype=int) preshape[1, 0] = int(state.dimension / ((d**n)**(1 + apply_to[n_op - 1]))) if n_op > 1: preshape[1, 1:] = np.flip((d**n)**np.diff(apply_to)) / (d**n) shape1 = np.zeros(2 * n_op + 1, dtype=int) shape2 = np.zeros(2 * n_op + 1, dtype=int) order1 = np.zeros(2 * n_op + 1, dtype=int) order2 = np.zeros(2 * n_op + 1, dtype=int) shape1[:-1] = np.flip(preshape, axis=0).reshape((2 * n_op), order='F') shape1[-1] = -1 shape2[:-1] = preshape.reshape((-1), order='C') shape2[-1] = -1 preorder = np.arange(2 * n_op) order1[:-1] = np.flip(preorder.reshape((-1, 2), order='C'), axis=1).reshape((-1), order='F') order2[:-1] = np.flip(preorder.reshape((2, -1), order='C'), axis=0).reshape((-1), order='F') order1[-1] = 2 * n_op order2[-1] = 2 * n_op # Now left multiply out = state.reshape(shape1, order='F').transpose(order1) out = np.dot(op, out.reshape(((d**n)**n_op, -1), order='F')) out = out.reshape(shape2, order='F').transpose(order2) out = out.reshape(state.shape, order='F') return State(out, is_ket=state.is_ket, IS_subspace=state.IS_subspace, code=state.code) else: # Need to reshape the operator given new_shape = (d**n) * np.ones(2 * n_op, dtype=int) permut = np.argsort(apply_to) transpose_ord = np.zeros(2 * n_op, dtype=int) transpose_ord[:n_op] = permut transpose_ord[n_op:] = n_op * np.ones(n_op, dtype=int) + permut sorted_op = np.reshape(np.transpose(np.reshape(op, new_shape, order='F'), axes=transpose_ord), ((d**n)**n_op, (d**n)**n_op), order='F') sorted_apply_to = apply_to[permut] return left_multiply(state, sorted_apply_to, sorted_op) else: # op should be a list of Pauli operators, or out = state.copy() for i in range(len(apply_to)): if op[i] == 'X': # Sigma_X out = qubit.left_multiply(out, [n * apply_to[i]], ['X']) elif op[i] == 'Y': # Sigma_Y out = qubit.left_multiply( out, [n * apply_to[i], n * apply_to[i] + 1], ['Y', 'Z']) elif op[i] == 'Z': # Sigma_Z out = qubit.left_multiply( out, [n * apply_to[i], n * apply_to[i] + 1], ['Z', 'Z']) return State(out, is_ket=state.is_ket, IS_subspace=state.IS_subspace, code=state.code)
def right_multiply(state: State, apply_to: Union[int, list], op): """ Apply a multi-qubit operator on several qubits (indexed in apply_to) of the input codes. :param state: input wavefunction or density matrix :type state: np.ndarray :param apply_to: zero-based indices of qudit locations to apply the operator :type apply_to: list of int :param op: Operator to act with. :type op: np.ndarray (2-dimensional) """ if isinstance(apply_to, int): apply_to = [apply_to] if not isinstance(op, list): op = [op] # Type handling: to determine if Pauli, check if a list of strings pauli = False if isinstance(op, list): if all(isinstance(elem, str) for elem in op): pauli = True else: op = tools.tensor_product(op) if state.is_ket: print( 'Warning: right multiply functionality currently applies the operator and daggers the s.' ) n_op = len(apply_to) if not pauli: if tools.is_sorted(apply_to): # generate necessary shapes preshape = (d**n) * np.ones((2, n_op), dtype=int) preshape[0, 0] = int(state.dimension / ((d**n)**(1 + apply_to[n_op - 1]))) if n_op > 1: preshape[0, 1:] = np.flip((d**n)**np.diff(apply_to)) / (d**n) shape3 = np.zeros(2 * n_op + 2, dtype=int) shape3[0] = state.dimension shape3[1:-1] = np.reshape(preshape, (2 * n_op), order='F') shape3[-1] = -1 shape4 = np.zeros(2 * n_op + 2, dtype=int) shape4[0] = state.dimension shape4[1:n_op + 1] = preshape[0] shape4[n_op + 1] = -1 shape4[n_op + 2:] = preshape[1] order3 = np.zeros(2 * n_op + 2, dtype=int) order3[0] = 0 order3[1:n_op + 2] = 2 * np.arange(n_op + 1) + np.ones(n_op + 1) order3[n_op + 2:] = 2 * np.arange(1, n_op + 1) order4 = np.zeros(2 * n_op + 2, dtype=int) order4[0] = 0 order4[1] = 1 order4[2:] = np.flip(np.arange(2, 2 * n_op + 2).reshape((2, -1), order='C'), axis=0).reshape((-1), order='F') # right multiply out = state.reshape(shape3, order='F').transpose(order3) out = np.dot(out.reshape((-1, (d**n)**n_op), order='F'), op.conj().T) out = out.reshape(shape4, order='F').transpose(order4) out = out.reshape(state.shape, order='F') return State(out, is_ket=state.is_ket, IS_subspace=state.IS_subspace, code=state.code) else: new_shape = 2 * np.ones(2 * n_op, dtype=int) permut = np.argsort(apply_to) transpose_ord = np.zeros(2 * n_op, dtype=int) transpose_ord[:n_op] = permut transpose_ord[n_op:] = n_op * np.ones(n_op, dtype=int) + permut sorted_op = np.reshape(np.transpose(np.reshape(op, new_shape, order='F'), axes=transpose_ord), ((d**n)**n_op, (d**n)**n_op), order='F') sorted_apply_to = apply_to[permut] return right_multiply(state, sorted_apply_to, sorted_op) else: out = state.copy() for i in range(len(apply_to)): # Note index start from the right (sN,...,s3,s2,s1) if op[i] == 'X': # Sigma_X out = qubit.left_multiply(out, [n * apply_to[i]], ['X']) elif op[i] == 'Y': # Sigma_Y out = qubit.left_multiply( out, [n * apply_to[i], n * apply_to[i] + 1], ['Y', 'Z']) elif op[i] == 'Z': # Sigma_Z out = qubit.left_multiply( out, [n * apply_to[i], n * apply_to[i] + 1], ['Z', 'Z']) return State(out, is_ket=state.is_ket, IS_subspace=state.IS_subspace, code=state.code)
def channel(self, state: State, p: float, apply_to: Union[int, list] = None): """ Applies ``povm`` homogeneously to the qudits identified in apply_to. :param p: :param apply_to: :param state: State to operate on. :type state: np.ndarray :return: """ # If the input s is a ket, convert it to a density matrix if state.is_ket: print('Converting ket to density matrix.') state = State(tools.outer_product(state, state)) if apply_to is None: apply_to = list(range(state.number_physical_qudits)) # Assume that apply_to is a list of integers if isinstance(apply_to, int): apply_to = [apply_to] if state.code.logical_code: # Assume that logical codes are composed of qubits code = self.code else: code = state.code if self.IS_subspace: povm = self.povm(p) temp = state.copy() out = None for i in apply_to: out = State(np.zeros_like(state), is_ket=state.is_ket, code=state.code, IS_subspace=state.IS_subspace, graph=state.graph) for j in range(len(povm[i])): out = out + povm[i][j] @ temp @ povm[i][j].conj().T temp = out return out # Handle apply_to recursively # Only apply to one qudit else: # Empty s to store the output out = State(np.zeros_like(state), is_ket=state.is_ket, code=state.code, IS_subspace=state.IS_subspace, graph=state.graph) if len(apply_to) == 1: povm = self.povm(p) for j in range(len(povm)): out = out + code.multiply(state, apply_to, povm[j]) return out else: last_element = apply_to.pop() recursive_solution = self.channel(state, p, apply_to=apply_to) povm = self.povm(p) for j in range(len(povm)): out = out + code.multiply(recursive_solution, [last_element], povm[j]) return out