Source code for c3.utils.qt_utils

"""Useful functions to get basis vectors and matrices of the right size."""
import itertools
import numpy as np
from typing import List
from scipy.linalg import block_diag as scipy_block_diag
from scipy.linalg import expm
from c3.libraries.constants import Id, X, Y, Z, PAULIS, CLIFFORDS


[docs]def pauli_basis(dims=[2]): """ Qutip implementation of the Pauli basis. Parameters ---------- dims : list List of dimensions of each subspace. Returns ------- np.array A square matrix containing the Pauli basis of the product space """ _SINGLE_QUBIT_PAULI_BASIS = (Id, X, Y, Z) paulis = [] for dim in dims: paulis.append([expand_dims(P, dim) for P in _SINGLE_QUBIT_PAULI_BASIS]) result = [[]] res_tuple = [] # TAKEN FROM ITERTOOLS for pauli_set in paulis: result = [x + [y] for x in result for y in pauli_set] for prod in result: res_tuple.append(tuple(prod)) # TAKEN FROM QUTIP size = np.prod(np.array(dims) ** 2) B = np.zeros((size, size), dtype=complex) for idx, op_tuple in enumerate(res_tuple): op = np_kron_n(op_tuple) vec = np.reshape(np.transpose(op), [-1, 1]) B[:, idx] = vec.T.conj() return B
[docs]def expand_dims(op, dim): """ pad operator with zeros to be of dimension dim Attention! Not related to the TensorFlow function """ op_out = np.zeros([dim, dim], dtype=op.dtype) op_out[: op.shape[0], : op.shape[1]] = op return op_out
# MATH HELPERS
[docs]def np_kron_n(mat_list): """ Apply Kronecker product to a list of matrices. """ tmp = np.eye(1) for m in mat_list: tmp = np.kron(tmp, m) return tmp
[docs]def hilbert_space_kron(op, indx, dims): """ Extend an operator op to the full product hilbert space given by dimensions in dims. Parameters ---------- op : np.array Operator to be extended. indx : int Position of which subspace to extend. dims : list New dimensions of the subspace. Returns ------- np.array Extended operator. """ op_list = [] for indy in range(len(dims)): qI = np.identity(dims[indy]) if indy == indx: op_list.append(op) else: op_list.append(qI) if indx > len(dims) - 1: raise Warning(f"Index {indx} is outside the Hilbert space dimensions {dims}. ") return np_kron_n(op_list)
[docs]def rotation(phase: float, xyz: np.array) -> np.array: """General Rotation using Euler's formula. Parameters ---------- phase : np.float Rotation angle. xyz : np.array Normal vector of the rotation axis. Returns ------- np.array Unitary matrix """ rot = np.cos(phase / 2) * Id - 1j * np.sin(phase / 2) * ( xyz[0] * X + xyz[1] * Y + xyz[2] * Z ) return rot
[docs]def basis(lvls: int, pop_lvl: int) -> np.array: """ Construct a basis state vector. Parameters ---------- lvls : int Dimension of the state. pop_lvl : int The populated entry. Returns ------- np.array A normalized state vector with one populated entry. """ psi = np.zeros([lvls, 1]) psi[pop_lvl] = 1 return psi
[docs]def xy_basis(lvls: int, vect: str): """ Construct basis states on the X, Y and Z axis. Parameters ---------- lvls : int Dimensions of the Hilbert space. vect : str Identifier of the state. Options: 'zp', 'zm', 'xp', 'xm', 'yp', 'ym' Returns ------- np.array A state on one of the axis of the Bloch sphere. """ psi_g = basis(lvls, 0) psi_e = basis(lvls, 1) basis_states = { "zm": psi_g, "zp": psi_e, "xp": (psi_g + psi_e) / np.sqrt(2), "xm": (psi_g - psi_e) / np.sqrt(2), "yp": (psi_g + 1.0j * psi_e) / np.sqrt(2), "ym": (psi_g - 1.0j * psi_e) / np.sqrt(2), } try: psi = basis_states[vect] except KeyError: print("vect must be one of 'zp' 'zm' 'xp' 'xm' 'yp' 'ym'") psi = None return psi
[docs]def projector(dims, indices, outdims=None): """ Computes the projector to cut down a matrix to the computational space. The subspaces indicated in indeces will be projected to the lowest two states, the rest is projected onto the lowest state. If outdims is defined projection will be performed to those states. """ if outdims is None: outdims = [2] * len(dims) ids = [] for index, dim in enumerate(dims): outdim = outdims[index] if index in indices: ids.append(np.eye(dim, outdim)) else: ids.append(np.eye(dim, 1)) return np_kron_n(ids)
[docs]def kron_ids(dims, indices, matrices): """ Kronecker product of matrices at specified indices with identities everywhere else. """ ids = [] for index, dim in enumerate(dims): ids.append(np.eye(dim)) for index, matrix in enumerate(matrices): ids[indices[index]] = matrix return np_kron_n(ids)
[docs]def get_basis_matrices(dim): """ Basis matrices with single ones of the matrices with given dimensions. """ basis_mats = list() for i in range(dim**2): b = np.zeros((dim, dim), dtype=np.complex128) b.flat[i] = 1 basis_mats.append(b) return basis_mats
[docs]def insert_mat_kron(dims, target_ids, matrix) -> np.ndarray: """ Insert matrix at given indices. All other spaces are filled with zeros. Parameters ---------- dims: dimensions of each qubit subspace target_ids: qubits to apply matrix to matrix: matrix to be applied Returns ------- composed matrix """ seperated_basis_matrices = list( itertools.product(*[get_basis_matrices(dims[target]) for target in target_ids]) ) tot_dim = np.product(dims) out_matrix = np.zeros((tot_dim, tot_dim), dtype=np.complex128) for basis_mats in seperated_basis_matrices: kron_mat = np_kron_n(basis_mats) norm = np.sqrt(np.sum(np.abs(kron_mat))) kron_mat /= norm expanded_mat = kron_ids(dims, target_ids, basis_mats) / norm # calculate overlap of initial matrix with element overlap = np.sum(matrix * kron_mat) out_matrix += overlap * expanded_mat return out_matrix
[docs]def pad_matrix(matrix, dim, padding): """ Fills matrix dimensions with zeros or identity. """ if padding == "compsub": return matrix elif padding == "wzeros": zeros = np.zeros([dim, dim]) matrix = scipy_block_diag(matrix, zeros) elif padding == "fulluni": identity = np.eye(dim) matrix = scipy_block_diag(matrix, identity) return matrix
# NOTE: Removed perfect_gate() as in commit 0f7cba3, replaced by explicit constants in # c3/libraries/constants.py
[docs]def perfect_parametric_gate(paulis_str, ang, dims): """ Construct an ideal parametric gate. Parameters ---------- paulis_str : str Names for the Pauli matrices that identify the rotation axis. Example: - "X" for a single-qubit rotation about the X axis - "Z:X" for an entangling rotation about Z on the first and X on the second qubit ang : float Angle of the rotation dims : list Dimensions of the subspaces. Returns ------- np.array Ideal gate. """ ps = [] p_list = paulis_str.split(":") for idx, key in enumerate(p_list): if key not in PAULIS: raise KeyError( f"Incorrect pauli matrix {key} in position {idx}.\ Select from {PAULIS.keys()}." ) ps.append(pad_matrix(PAULIS[key], dims[idx] - 2, "wzeros")) gen = np_kron_n(ps) return expm(-1.0j / 2 * ang * gen)
[docs]def perfect_single_q_parametric_gate(pauli_str, target, ang, dims): """ Construct an ideal parametric gate. Parameters ---------- paulis_str : str Name for the Pauli matrices that identify the rotation axis. Example: - "X" for a single-qubit rotation about the X axis ang : float Angle of the rotation dims : list Dimensions of the subspaces. Returns ------- np.array Ideal gate. """ ps = [] p_list = ["Id"] * len(dims) p_list[target] = pauli_str for idx, key in enumerate(p_list): if key not in PAULIS: raise KeyError( f"Incorrect pauli matrix {key} in position {idx}.\ Select from {PAULIS.keys()}." ) ps.append(pad_matrix(PAULIS[key], dims[idx] - 2, "wzeros")) gen = np_kron_n(ps) return expm(-1.0j / 2 * ang * gen)
[docs]def two_qubit_gate_tomography(gate): """ Sequences to generate tomography for evaluating a two qubit gate. """ # THE 4 GATES base = [["Id", "Id"], ["rx90p", "Id"], ["ry90p", "Id"], ["rx90p", "rx90p"]] base2 = [] for x in base: for y in base: g = [] for indx in range(2): g.append(x[indx] + ":" + y[indx]) base2.append(g) S = [] for x in base2: for y in base2: g = [] for g1 in x: g.append(g1) g.append(gate) for g2 in y: g.append(g2) S.append(g) return S
[docs]def T1_sequence(length, target): """ Generate a gate sequence to measure relaxation time in a two-qubit chip. Parameters ---------- length : int Number of Identity gates. target : int Which qubit is measured. Returns ------- list Relaxation sequence. """ wait = ["Id"] prepare_1 = [f"rx90p[{str(target)}]"] * 2 S = [] S.extend(prepare_1) S.extend(wait * length) return S
[docs]def ramsey_sequence(length, target): """ Generate a gate sequence to measure dephasing time in a two-qubit chip. Parameters ---------- length : int Number of Identity gates. target : str Which qubit is measured. Options: "left" or "right" Returns ------- list Dephasing sequence. """ wait = ["id"] rotate_90 = [f"rx90p[{str(target)}]"] S = [] S.extend(rotate_90) S.extend(wait * length) S.extend(rotate_90) return S
[docs]def ramsey_echo_sequence(length, target): """ Generate a gate sequence to measure dephasing time in a two-qubit chip including a flip in the middle. This echo reduce effects detrimental to the dephasing measurement. Parameters ---------- length : int Number of Identity gates. Should be even. target : str Which qubit is measured. Options: "left" or "right" Returns ------- list Dephasing sequence. """ wait = ["id"] hlength = length // 2 rotate_90_p = [f"rx90p[{str(target)}]"] rotate_90_m = [f"rx90m[{str(target)}]"] S = [] S.extend(rotate_90_p) S.extend(wait * hlength) S.extend(rotate_90_p) S.extend(rotate_90_p) S.extend(wait * hlength) S.extend(rotate_90_m) return S
[docs]def single_length_RB( RB_number: int, RB_length: int, target: int = 0 ) -> List[List[str]]: """Given a length and number of repetitions it compiles Randomized Benchmarking sequences. Parameters ---------- RB_number : int The number of sequences to construct. RB_length : int The number of Cliffords in each individual sequence. target : int Index of the target qubit Returns ------- list List of RB sequences. """ S = [] for _ in range(RB_number): seq = np.random.choice(24, size=RB_length - 1) + 1 seq = np.append(seq, inverseC(seq)) seq_gates = [] for cliff_num in seq: g = [f"{c}[{target}]" for c in cliffords_decomp[cliff_num - 1]] seq_gates.extend(g) S.append(seq_gates) return S
[docs]def inverseC(sequence): """Find the clifford to end a sequence s.t. it returns identity.""" operation = Id for cliff in sequence: gate_str = "C" + str(cliff) gate = CLIFFORDS[gate_str] operation = gate @ operation for i in range(1, 25): inv = CLIFFORDS["C" + str(i)] trace = np.trace(inv @ operation) if abs(2 - abs(trace)) < 0.0001: return i
[docs]def perfect_cliffords(lvls: List[int], proj: str = "fulluni", num_gates: int = 1): """ Legacy function to compute the clifford gates. """ return CLIFFORDS
cliffords_string = [ "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "C10", "C11", "C12", "C13", "C14", "C15", "C16", "C17", "C18", "C19", "C20", "C21", "C22", "C23", "C24", ] cliffords_decomp = [ ["rx90p", "rx90m"], ["ry90p", "rx90p"], ["rx90m", "ry90m"], ["ry90p", "rx90p", "rx90p"], ["rx90m"], ["rx90p", "ry90m", "rx90m"], ["rx90p", "rx90p"], ["ry90m", "rx90m"], ["rx90p", "ry90m"], ["ry90m"], ["rx90p"], ["rx90p", "ry90p", "rx90p"], ["ry90p", "ry90p"], ["ry90m", "rx90p"], ["rx90p", "ry90p"], ["ry90m", "rx90p", "rx90p"], ["rx90p", "ry90p", "ry90p"], ["rx90p", "ry90m", "rx90p"], ["rx90p", "rx90p", "ry90p", "ry90p"], ["ry90p", "rx90m"], ["rx90m", "ry90p"], ["ry90p"], ["rx90m", "ry90p", "ry90p"], ["rx90p", "ry90p", "rx90m"], ] # TODO: Deal with different decompositions # cliffords_decomp = [ # ['Id', 'Id', 'Id', 'Id'], # ['ry90p', 'rx90p', 'Id', 'Id'], # ['rx90m', 'ry90m', 'Id', 'Id'], # ['ry90p', 'rx90p', 'rx90p', 'Id'], # ['rx90m', 'Id', 'Id', 'Id'], # ['rx90p', 'ry90m', 'rx90m', 'Id'], # ['rx90p', 'rx90p', 'Id', 'Id'], # ['ry90m', 'rx90m', 'Id', 'Id'], # ['rx90p', 'ry90m', 'Id', 'Id'], # ['ry90m', 'Id', 'Id', 'Id'], # ['rx90p', 'Id', 'Id', 'Id'], # ['rx90p', 'ry90p', 'rx90p', 'Id'], # ['ry90p', 'ry90p', 'Id', 'Id'], # ['ry90m', 'rx90p', 'Id', 'Id'], # ['rx90p', 'ry90p', 'Id', 'Id'], # ['ry90m', 'rx90p', 'rx90p', 'Id'], # ['rx90p', 'ry90p', 'ry90p', 'Id'], # ['rx90p', 'ry90m', 'rx90p', 'Id'], # ['rx90p', 'rx90p', 'ry90p', 'ry90p'], # ['ry90p', 'rx90m', 'Id', 'Id'], # ['rx90m', 'ry90p', 'Id', 'Id'], # ['ry90p', 'Id', 'Id', 'Id'], # ['rx90m', 'ry90p', 'ry90p', 'Id'], # ['rx90p', 'ry90p', 'rx90m', 'Id'] # ] cliffords_decomp_xId = [[gate + ":Id" for gate in clif] for clif in cliffords_decomp] sum = 0 for cd in cliffords_decomp: sum = sum + len(cd) cliffors_per_gate = sum / len(cliffords_decomp) # TODO: Deal with different decompositions # cliffords_decomp = [ # ['rx90p', 'rx90m'], # ['rx90p', 'rx90p'], # ['ry90p', 'ry90p'], # ['RZ90p', 'RZ90p'], # ['rx90p'], # ['ry90p'], # ['RZ90p'], # ['rx90m'], # ['ry90m'], # ['RZ90m'], # ['RZ90p', 'rx90p'], # ['RZ90p', 'RZ90p', 'rx90m'], # ['RZ90p', 'rx90p', 'rx90p'], # ['RZ90m', 'rx90p', 'rx90p'], # ['RZ90p', 'rx90p'], # ['RZ90p', 'rx90m'], # ['rx90p', 'RZ90m'], # ['RZ90p', 'RZ90p', 'ry90m'], # ['RZ90p', 'ry90m'], # ['RZ90m', 'ry90p'], # ['RZ90p', 'RZ90p', 'ry90p'], # ['RZ90m', 'rx90p'], # ['RZ90p', 'ry90p'], # ['RZ90m', 'rx90m'] # ] # # cliffords_decomp = [ # ['rx90p', 'rx90m'], # ['rx90p', 'rx90p'], # ['ry90p', 'ry90p'], # ['ry90p', 'rx90p', 'rx90p', 'ry90m'], # ['rx90p'], # ['ry90p'], # ['ry90p', 'rx90p', 'ry90m'], # ['rx90m'], # ['ry90m'], # ['rx90p', 'ry90p', 'rx90m'], # ['ry90p', 'rx90p', 'ry90m', 'rx90p'], # ['ry90p', 'rx90p', 'rx90p', 'ry90m', 'rx90m'], # ['ry90p', 'rx90p', 'ry90m', 'rx90p', 'rx90p'], # ['rx90p', 'ry90p', 'rx90m', 'rx90p', 'rx90p'], # ['ry90p', 'rx90p', 'ry90m', 'rx90p'], # ['ry90p', 'rx90p', 'ry90m', 'rx90m'], # ['rx90p', 'rx90p', 'ry90p', 'rx90m'], # ['ry90p', 'rx90p', 'rx90p', 'ry90m', 'ry90m'], # ['ry90p', 'rx90p', 'ry90m', 'ry90m'], # ['rx90p', 'ry90p', 'rx90m', 'ry90p'], # ['ry90p', 'rx90p', 'rx90p'], # ['rx90p', 'ry90p'], # ['ry90p', 'rx90p'], # ['rx90p', 'ry90p', 'rx90m', 'rx90m'] # ] # # cliffords_decomp = [ # ['rx90p', 'rx90m'], # ['ry90p', 'rx90p'], # ['rx90m', 'ry90m'], # ['ry90p', 'rxp'], # ['rx90m'], # ['rx90p', 'ry90m', 'rx90m'], # ['rxp'], # ['ry90m', 'rx90m'], # ['rx90p', 'ry90m'], # ['ry90m'], # ['rx90p'], # ['rx90p', 'ry90p', 'rx90p'], # ['ryp'], # ['ry90m', 'rx90p'], # ['rx90p', 'ry90p'], # ['ry90m', 'rxp'], # ['rx90p', 'ryp'], # ['rx90p', 'ry90m', 'rx90p'], # ['rxp', 'ryp'], # ['ry90p', 'rx90m'], # ['rx90m', 'ry90p'], # ['ry90p'], # ['rx90m', 'ryp'], # ['rx90p', 'ry90p', 'rx90m'] # ]