Source code for geqo.utils._cupy_.helpers

try:
    import cupy as cp

    try:
        cp.cuda.Device(0).compute_capability
        use_cupy = True
    except Exception:
        import numpy as cp

        use_cupy = False
except ImportError:
    import numpy as cp

    use_cupy = False


[docs] def cupyWarmup(): """Warm up the GPU. It's a good practice to run this function before simulating a large circuit for the first time, as the initial CUDA API call might take some time to process. For more information please refer to: https://docs.cupy.dev/en/stable/user_guide/performance.html""" a = cp.random.rand(100, 100) @ cp.random.rand(100, 1) b = cp.random.rand(100) * cp.random.rand(100) cp.dot(a.ravel(), b) d = cp.zeros((1024, 1024), dtype=cp.complex128) d[cp.arange(512)[:, None], cp.arange(512)[None, :]] = 1.0 + 1.0j
# cp.cuda.Device(0).synchronize()
[docs] def getRXCupy(angle): """Return CuPy matrix for Rx gate with given angle.""" return cp.array( [ [cp.cos(angle / 2), -1j * cp.sin(angle / 2)], [-1j * cp.sin(angle / 2), cp.cos(angle / 2)], ], dtype=cp.complex128, )
[docs] def getRYCupy(angle): """Return CuPy matrix for Ry gate with given angle.""" return cp.array( [ [cp.cos(angle / 2), -cp.sin(angle / 2)], [cp.sin(angle / 2), cp.cos(angle / 2)], ], dtype=cp.complex128, )
[docs] def getQFTCuPy(n, inverse): """Return CuPy matrix for QFT.""" sign = -1 if inverse else 1 N = 2**n omega = cp.exp(sign * 2 * cp.pi * 1j / N) j = cp.arange(N).reshape((N, 1)) k = cp.arange(N).reshape((1, N)) qft = omega ** (j * k) qft = qft / cp.sqrt(N) return qft.astype(cp.complex128)
[docs] def permutationMatrixCupy(perm: list): """Return cupy permutation matrix for given qubit permutation.""" n = len(perm) dim = 2**n # Create all binary representations at once binary_indices = cp.arange(dim)[:, None] >> cp.arange(n)[None, ::-1] & 1 # >> n shift the binary string by n bits to the right (i.e. 0100 >> 2 = 0001) # & 1 returns 0 if the number is even and 1 if odd. (i.e. 3 & 1 = 1) # Apply the permutation to each binary representation permuted_binary = binary_indices[:, perm] # Convert binary arrays to decimal by vectorization powers = cp.array([2 ** (n - i - 1) for i in range(n)]) row_indices = cp.sum(binary_indices * powers, axis=1) col_indices = cp.sum(permuted_binary * powers, axis=1) result = cp.zeros((dim, dim)) result[row_indices, col_indices] = 1 return result
[docs] def partialTraceCupy(rho, qubits, dropTargets): """Fully vectorized partial trace implementation without any loops.""" undroppedQubits = [q for q in qubits if q not in dropTargets] targetOrder2 = undroppedQubits + dropTargets perm = permutationMatrixCupy([targetOrder2.index(q) for q in qubits]) rho2 = perm @ rho @ perm.T # Calculate dimensions n_kept = len(undroppedQubits) n_traced = len(dropTargets) dim_kept = 2**n_kept dim_traced = 2**n_traced # Reshape the density matrix to separate kept and traced-out subsystems # Each of the first two and last two dimensions forms an axis corresponding to the full Hilbert space of the system (2^num_qubits) rho_reshaped = rho2.reshape(dim_kept, dim_traced, dim_kept, dim_traced) # Perform the partial trace using einsum # Sum over the second and fourth dimensions (traced-out subsystems) rhoNew = cp.einsum("jiki->jk", rho_reshaped) return rhoNew, perm
[docs] def partial_diag_cupy(rho, qubits, dropTargets): """Compute partial trace over dropTargets and return non-zero diagonal values with bitstring indices.""" n = len(qubits) dropindex = sorted([qubits.index(i) for i in dropTargets]) keepindex = [i for i in range(n) if i not in dropindex] dim = 2**n diag = cp.diag(rho) # diagonal elements # Generate all binary representations of indices indices = cp.arange(dim) bits = (indices[:, None] >> cp.arange(n - 1, -1, -1)) & 1 # Keep only bits that are not traced out kept_bits = bits[:, keepindex] # Convert each kept_bits row to an integer key powers = 2 ** cp.arange(len(keepindex) - 1, -1, -1) keys = (kept_bits * powers).sum(axis=1) # Sum up diagonals with the same kept qubits key using bincount probs = cp.bincount(keys, weights=cp.real(diag), minlength=2 ** len(keepindex)) # Get nonzero indices and values nonzero_idx = cp.nonzero(probs)[0] values = probs[nonzero_idx] # Convert indices to binary vectors result = [] for i in range(len(nonzero_idx)): if ( values[i] > 0 ): # some very small negative values might occur due to numpy numerical errors # bin_vec = cp.array( # [(nonzero_idx[i] >> j) & 1 for j in range(len(keepindex) - 1, -1, -1)] # ).get() # convert bit vector to numpy array (when using cupy) bin_vec = cp.array( [(nonzero_idx[i] >> j) & 1 for j in range(len(keepindex) - 1, -1, -1)] ) result.append((bin_vec, values[i])) return result
[docs] def projection_cupy(densityMatrix, num_qubits, targets, basis): """Construct the projector of a given basis state and compute the projected density matrix.""" zero = cp.array([1.0, 0.0]) one = cp.array([0.0, 1.0]) identity = cp.array([1.0, 1.0]) sorted_targets = cp.sort(cp.array(targets)) # construct the projector vectors = [] for i in range(num_qubits): if cp.any(sorted_targets == i): idx = cp.searchsorted(sorted_targets, i) # Find the index in sorted_targets component = zero if basis[idx] == 0 else one vectors.append(component) else: vectors.append(identity) def tensor_product(vectors): """Compute the tensor products for a given list of arrays.""" Vector = vectors[0] for vec in vectors[1:]: Vector = cp.kron(Vector, vec) return Vector proj = tensor_product(vectors) # projector # projector applied to the density matrix nonzero = cp.where(proj != 0)[0] resultRho = cp.zeros_like(densityMatrix, dtype=cp.complex128) # This part uses the vectorized indexing, which facilitates the element assignment subroutine rows = nonzero[:, None] # shape (len(nonzero),1) cols = nonzero[None, :] # shape (1,len(nonzero)) resultRho[rows, cols] = densityMatrix[rows, cols] return resultRho
[docs] def multiQubitsUnitaryCupy(u, qubits, targets): """ compute the whole-system unitary `U` given the target qubits and the target unitary `u`. """ targets = [qubits.index(t) for t in targets] non_targets = [qubits.index(i) for i in qubits if i not in targets] num_qubits = len(qubits) # Create non-target combinations and compute their contribution to index non_target_bin = ( cp.arange(2 ** len(non_targets))[:, None] >> cp.arange(len(non_targets))[::-1] ) & 1 powers_non = 2 ** cp.array([num_qubits - 1 - i for i in non_targets]) non_idx = (non_target_bin * powers_non).sum(axis=1) # shape (2^(n-t),) # Create target combinations (from min to max) and sorted target combination according to the target order target_bin = ( cp.arange(2 ** len(targets))[:, None] >> cp.arange(len(targets))[::-1] ) & 1 perm = [sorted(targets).index(t) for t in targets] sorted_bin = target_bin[:, perm] # Compute the target contribution of the large U indices powers_tar = 2 ** cp.array([num_qubits - 1 - i for i in sorted(targets)]) U_idx = (target_bin * powers_tar).sum(axis=1) # shape (2^t,) # Compute the small u indices from the sorted target order (target = [q2,q0], nontarget=[q1] then the original indexing should be reversed) powers_sort_tar = 2 ** (len(targets) - 1 - cp.arange(len(targets))) u_idx = (sorted_bin * powers_sort_tar).sum(axis=1).astype(cp.int64) # Compute all full-system U indices (target + non_target contribution) idx_rows = non_idx[:, None] + U_idx[None, :] # shape (2^(n-t), 2^t) idx_rows_flat = idx_rows.reshape(-1) # Get all values from smaller u according to the sorted target indices u_matrix = u[u_idx[:, None], u_idx[None, :]] # shape (2^t, 2^t) tiled_u = cp.tile( u_matrix, (2 ** len(non_targets), 1) ) # shape (2^(n-t)*2^t, 2^t) repeat u_matrix 2^(n-t) times along the row and 1 time along the column idx_cols = cp.tile(idx_rows, (1, 2 ** len(targets))).reshape( -1 ) # repeat idx_rows 1 time along the row and 2^t times along the column # Broadcast rows for assigning repeated_rows = cp.repeat( idx_rows_flat, 2 ** len(targets) ) # repeat every element 4 times i.e. [1,2] => [1,1,1,1,2,2,2,2] # Element assignment U = cp.zeros((2**num_qubits, 2**num_qubits), dtype=cp.complex128) U[repeated_rows.astype(cp.int64), idx_cols.astype(cp.int64)] = tiled_u.reshape(-1) return U
'''def unitaryDensityMatrixCupy(u, rho, num_qubits, targets): """Apply unitary transformation to part of the density matrix associated with the target qubits without permutation or full matrix multiplication.""" non_targets = list(set(range(num_qubits)) - set(targets)) non_targets = sorted(non_targets) # Create non-target combinations and compute their contribution to index non_target_bin = ( cp.arange(2 ** len(non_targets))[:, None] >> cp.arange(len(non_targets))[::-1] ) & 1 powers_non = 2 ** cp.array([num_qubits - 1 - i for i in non_targets]) non_idx = (non_target_bin * powers_non).sum(axis=1) # shape (2^(n-t),) # Create target combinations (from min to max) and sorted target combination according to the target order target_bin = ( cp.arange(2 ** len(targets))[:, None] >> cp.arange(len(targets))[::-1] ) & 1 perm1 = [sorted(targets).index(t) for t in targets] sorted_bin_before_u = target_bin[ :, perm1 ] # sort the density matrix before application of unitary perm2 = [targets.index(t) for t in sorted(targets)] sorted_bin_after_u = target_bin[ :, perm2 ] # sort the density matrix after application of unitary # Compute the target contribution of the large U indices powers_tar = 2 ** cp.array([num_qubits - 1 - i for i in sorted(targets)]) U_idx = (target_bin * powers_tar).sum(axis=1) # shape (2^t,) # Compute the small u indices from the sorted target order (target = [q2,q0], nontarget=[q1] then the original indexing should be reversed) powers_sort_tar = 2 ** (len(targets) - 1 - cp.arange(len(targets))) before_u_idx = ( (sorted_bin_before_u * powers_sort_tar).sum(axis=1).astype(cp.int64) ) # permuting index before applying u. shape (2^t) after_u_idx = ( (sorted_bin_after_u * powers_sort_tar).sum(axis=1).astype(cp.int64) ) # permuting index after applying u. shape (2^t) # Compute all full-system U indices (target + non_target contribution) idx_full_mat = non_idx[:, None] + U_idx[None, :] # shape (2^(n-t), 2^t) idx_full = idx_full_mat.reshape(-1).astype(cp.int64) # shape (2^n ,) # 1. extract rhos with different non-targets combinations rho_tensor = rho[ idx_full[:, None], idx_full[None, :] ] # a tensor of extracted rhos with different non_targ combinations [[(0,0),(0,1)],[(1,0),(1,1)]] # shape (2^n, 2^n) # 2. shuffle the extracted rhos to match the target order tiled_before_u_idx = ( cp.array( [before_u_idx + 2 ** len(targets) * i for i in range(2 ** len(non_targets))] ) .astype(cp.int64) .reshape(-1) ) # shape (2^n) rho_tensor = rho_tensor[tiled_before_u_idx[:, None], tiled_before_u_idx[None, :]] # 3. apply unitary transformation to all the extracted rhos transformed_rho = cp.empty_like(rho_tensor) for i in range(2 ** len(non_targets)): for j in range(2 ** len(non_targets)): transformed_rho[ 2 ** len(targets) * i : 2 ** len(targets) * (i + 1), 2 ** len(targets) * j : 2 ** len(targets) * (j + 1), ] = ( u @ rho_tensor[ 2 ** len(targets) * i : 2 ** len(targets) * (i + 1), 2 ** len(targets) * j : 2 ** len(targets) * (j + 1), ] @ u.conj().T ) # 4. shuffle the transformed rhos back to the sorted order tiled_after_u_idx = ( cp.array( [after_u_idx + 2 ** len(targets) * i for i in range(2 ** len(non_targets))] ) .astype(cp.int64) .reshape(-1) ) # shape (2^n) transformed_rho = transformed_rho[ tiled_after_u_idx[:, None], tiled_after_u_idx[None, :] ] # shape (2^n ,2^n) # 5. Assignment matrix element of new rho new_rho = cp.empty_like(transformed_rho) new_rho[idx_full[:, None], idx_full[None, :]] = transformed_rho return new_rho'''