Source code for c3.libraries.fidelities

"""Library of fidelity functions."""

import numpy as np
import tensorflow as tf
from typing import List, Dict

from scipy.optimize import curve_fit

from c3.signal.gates import Instruction
from c3.utils.tf_utils import (
    tf_ave,
    tf_super,
    tf_abs,
    tf_ketket_fid,
    tf_superoper_unitary_overlap,
    tf_unitary_overlap,
    tf_project_to_comp,
    tf_dm_to_vec,
    tf_average_fidelity,
    tf_superoper_average_fidelity,
    tf_state_to_dm,
)

from c3.libraries.propagation import evaluate_sequences

from c3.utils.qt_utils import (
    basis,
    perfect_cliffords,
    cliffords_decomp,
    cliffords_decomp_xId,
    single_length_RB,
    cliffords_string,
)

state_providers = dict()
unitary_providers = dict()
set_providers = dict()
super_providers = dict()

# Compatibility for legacy, i.e. not sorted yet
fidelities = dict()


[docs]def fid_reg_deco(func): """ Decorator for making registry of functions """ fidelities[str(func.__name__)] = func return func
[docs]def state_deco(func): """ Decorator for making registry of functions """ state_providers[str(func.__name__)] = func return func
[docs]def unitary_deco(func): """ Decorator for making registry of functions """ unitary_providers[str(func.__name__)] = func return func
[docs]def set_deco(func): """ Decorator for making registry of functions """ set_providers[str(func.__name__)] = func return func
[docs]def open_system_deco(func): """ Decorator for making registry of functions """ super_providers[str(func.__name__)] = func return func
[docs]@fid_reg_deco @state_deco def state_transfer_infid_set( propagators: dict, instructions: dict, index, dims, psi_0, n_eval=-1, proj=True ): """ Mean state transfer infidelity. Parameters ---------- propagators : dict Contains unitary representations of the gates, identified by a key. index : int Index of the qubit(s) in the Hilbert space to be evaluated dims : list List of dimensions of qubits psi_0 : tf.Tensor Initial state of the device proj : boolean Project to computational subspace Returns ------- tf.float State infidelity, averaged over the gates in propagators """ infids = [] for gate, propagator in propagators.items(): perfect_gate = instructions[gate].get_ideal_gate(dims) infid = state_transfer_infid(perfect_gate, propagator, index, dims, psi_0) infids.append(infid) return tf.reduce_mean(infids)
[docs]@fid_reg_deco @state_deco def state_transfer_infid(ideal: np.ndarray, actual: tf.constant, index, dims, psi_0): """ Single gate state transfer infidelity. The dimensions of psi_0 and ideal need to be compatible and index and dims need to project actual to these same dimensions. Parameters ---------- ideal: np.ndarray Contains ideal unitary representations of the gate actual: tf.Tensor Contains actual unitary representations of the gate index : int Index of the qubit(s) in the Hilbert space to be evaluated dims : list List of dimensions of qubits psi_0: tf.Tensor Initial state Returns ------- tf.float State infidelity for the selected gate """ actual_comp = tf_project_to_comp(actual, dims=dims, index=index) psi_ideal = tf.matmul(ideal, psi_0) psi_actual = tf.matmul(actual_comp, psi_0) overlap = tf_ketket_fid(psi_ideal, psi_actual) infid = 1 - overlap return infid
[docs]@fid_reg_deco @unitary_deco def unitary_infid( ideal: np.ndarray, actual: tf.Tensor, index: List[int] = None, dims=None ) -> tf.Tensor: """ Unitary overlap between ideal and actually performed gate. Parameters ---------- ideal : np.ndarray Ideal or goal unitary representation of the gate. actual : np.ndarray Actual, physical unitary representation of the gate. index : List[int] Index of the qubit(s) in the Hilbert space to be evaluated gate : str One of the keys of propagators, selects the gate to be evaluated dims : list List of dimensions of qubits Returns ------- tf.float Unitary fidelity. """ if index is None: index = list(range(len(dims))) actual_comp = tf_project_to_comp(actual, dims=dims, index=index) fid_lvls = 2 ** len(index) infid = 1 - tf_unitary_overlap(actual_comp, ideal, lvls=fid_lvls) return infid
[docs]@fid_reg_deco @unitary_deco @set_deco def unitary_infid_set(propagators: dict, instructions: dict, index, dims, n_eval=-1): """ Mean unitary overlap between ideal and actually performed gate for the gates in propagators. Parameters ---------- propagators : dict Contains actual unitary representations of the gates, resulting from physical simulation instructions : dict Contains the perfect unitary representations of the gates, identified by a key. index : List[int] Index of the qubit(s) in the Hilbert space to be evaluated dims : list List of dimensions of qubits n_eval : int Number of evaluation Returns ------- tf.float Unitary fidelity. """ infids = [] for gate, propagator in propagators.items(): perfect_gate = instructions[gate].get_ideal_gate(dims, index) infid = unitary_infid(perfect_gate, propagator, index, dims) infids.append(infid) return tf.reduce_mean(infids)
[docs]@fid_reg_deco @open_system_deco def lindbladian_unitary_infid( ideal: np.ndarray, actual: tf.constant, index=[0], dims=[2] ) -> tf.constant: """ Variant of the unitary fidelity for the Lindbladian propagator. Parameters ---------- ideal: np.ndarray Contains ideal unitary representations of the gate actual: tf.Tensor Contains actual unitary representations of the gate index : List[int] Index of the qubit(s) in the Hilbert space to be evaluated dims : list List of dimensions of qubits Returns ------- tf.float Overlap fidelity for the Lindblad propagator. """ U_ideal = tf_super(ideal) actual_comp = tf_project_to_comp(actual, dims=dims, index=index, to_super=True) fid_lvls = 2 ** len(index) infid = 1 - tf_superoper_unitary_overlap(actual_comp, U_ideal, lvls=fid_lvls) return infid
[docs]@fid_reg_deco @open_system_deco @set_deco def lindbladian_unitary_infid_set( propagators: dict, instructions: Dict[str, Instruction], index, dims, n_eval ): """ Variant of the mean unitary fidelity for the Lindbladian propagator. Parameters ---------- propagators : dict Contains actual unitary representations of the gates, resulting from physical simulation instructions : dict Contains the perfect unitary representations of the gates, identified by a key. index : List[int] Index of the qubit(s) in the Hilbert space to be evaluated dims : list List of dimensions of qubits n_eval : int Number of evaluation Returns ------- tf.float Mean overlap fidelity for the Lindblad propagator for all gates in propagators. """ infids = [] for gate, propagator in propagators.items(): perfect_gate = instructions[gate].get_ideal_gate(dims) infid = lindbladian_unitary_infid(perfect_gate, propagator, index, dims) infids.append(infid) return tf.reduce_mean(infids)
[docs]@fid_reg_deco @open_system_deco def average_infid( ideal: np.ndarray, actual: tf.Tensor, index: List[int] = [0], dims=[2] ) -> tf.constant: """ Average fidelity uses the Pauli basis to compare. Thus, perfect gates are always 2x2 (per qubit) and the actual unitary needs to be projected down. Parameters ---------- ideal: np.ndarray Contains ideal unitary representations of the gate actual: tf.Tensor Contains actual unitary representations of the gate index : List[int] Index of the qubit(s) in the Hilbert space to be evaluated dims : list List of dimensions of qubits """ actual_comp = tf_project_to_comp(actual, dims=dims, index=index) fid_lvls = [2] * len(index) infid = 1 - tf_average_fidelity(actual_comp, ideal, lvls=fid_lvls) return infid
[docs]@fid_reg_deco @open_system_deco @set_deco def average_infid_set( propagators: dict, instructions: dict, index: List[int], dims, n_eval=-1 ): """ Mean average fidelity over all gates in propagators. Parameters ---------- propagators : dict Contains unitary representations of the gates, identified by a key. index : int Index of the qubit(s) in the Hilbert space to be evaluated dims : list List of dimensions of qubits proj : boolean Project to computational subspace Returns ------- tf.float64 Mean average fidelity """ infids = [] for gate, propagator in propagators.items(): perfect_gate = instructions[gate].get_ideal_gate(dims, index) infid = average_infid(perfect_gate, propagator, index, dims) infids.append(infid) return tf.reduce_mean(infids)
[docs]@fid_reg_deco @open_system_deco @set_deco def average_infid_seq(propagators: dict, instructions: dict, index, dims, n_eval=-1): """ Average sequence fidelity over all gates in propagators. Parameters ---------- propagators : dict Contains unitary representations of the gates, identified by a key. index : int Index of the qubit(s) in the Hilbert space to be evaluated dims : list List of dimensions of qubits proj : boolean Project to computational subspace Returns ------- tf.float64 Mean average fidelity """ fid = 1 for gate, propagator in propagators.items(): perfect_gate = instructions[gate].get_ideal_gate(dims) fid *= 1 - average_infid(perfect_gate, propagator, index, dims) return 1 - fid
[docs]@fid_reg_deco @open_system_deco def lindbladian_average_infid( ideal: np.ndarray, actual: tf.constant, index=[0], dims=[2] ) -> tf.constant: """ Average fidelity uses the Pauli basis to compare. Thus, perfect gates are always 2x2 (per qubit) and the actual unitary needs to be projected down. Parameters ---------- ideal: np.ndarray Contains ideal unitary representations of the gate actual: tf.Tensor Contains actual unitary representations of the gate index : int Index of the qubit(s) in the Hilbert space to be evaluated dims : list List of dimensions of qubits """ U_ideal = tf_super(ideal) actual_comp = tf_project_to_comp(actual, dims=dims, index=index, to_super=True) infid = 1 - tf_superoper_average_fidelity(actual_comp, U_ideal, lvls=dims) return infid
[docs]@fid_reg_deco @open_system_deco @set_deco def lindbladian_average_infid_set( propagators: dict, instructions: Dict[str, Instruction], index, dims, n_eval ): """ Mean average fidelity over all gates in propagators. Parameters ---------- propagators : dict Contains unitary representations of the gates, identified by a key. index : int Index of the qubit(s) in the Hilbert space to be evaluated dims : list List of dimensions of qubits proj : boolean Project to computational subspace Returns ------- tf.float64 Mean average fidelity """ infids = [] for gate, propagator in propagators.items(): perfect_gate = instructions[gate].get_ideal_gate(dims) infid = lindbladian_average_infid(perfect_gate, propagator, index, dims) infids.append(infid) return tf.reduce_mean(infids)
[docs]@fid_reg_deco def epc_analytical(propagators: dict, index, dims, proj: bool, cliffords=False): # TODO check this work with new index and dims (double-check) num_gates = len(dims) if cliffords: real_cliffords = evaluate_sequences( propagators, [[C] for C in cliffords_string] ) elif num_gates == 1: real_cliffords = evaluate_sequences(propagators, cliffords_decomp) elif num_gates == 2: real_cliffords = evaluate_sequences(propagators, cliffords_decomp_xId) ideal_cliffords = perfect_cliffords(lvls=[2] * num_gates, num_gates=num_gates) fids = [] for C_indx in range(24): C_real = real_cliffords[C_indx] C_ideal = tf.constant(ideal_cliffords[C_indx], dtype=tf.complex128) ave_fid = tf_average_fidelity(C_real, C_ideal, lvls=dims) fids.append(ave_fid) infid = 1 - tf_ave(fids) return infid
[docs]@fid_reg_deco def lindbladian_epc_analytical( propagators: dict, index, dims, proj: bool, cliffords=False ): num_gates = len(dims) if cliffords: real_cliffords = evaluate_sequences( propagators, [[C] for C in cliffords_string] ) elif num_gates == 1: real_cliffords = evaluate_sequences(propagators, cliffords_decomp) elif num_gates == 2: real_cliffords = evaluate_sequences(propagators, cliffords_decomp_xId) ideal_cliffords = perfect_cliffords(lvls=[2] * num_gates, num_gates=num_gates) fids = [] for C_indx in range(24): C_real = real_cliffords[C_indx] C_ideal = tf_super(tf.constant(ideal_cliffords[C_indx], dtype=tf.complex128)) ave_fid = tf_superoper_average_fidelity(C_real, C_ideal, lvls=dims) fids.append(ave_fid) infid = 1 - tf_ave(fids) return infid
[docs]@fid_reg_deco def populations(state, lindbladian): if lindbladian: diag = [] dim = int(np.sqrt(len(state))) indeces = [n * dim + n for n in range(dim)] for indx in indeces: diag.append(state[indx]) return np.abs(diag) else: return np.abs(state) ** 2
[docs]@fid_reg_deco def population(propagators: dict, lvl: int, gate: str): U = propagators[gate] lvls = U.shape[0] psi_0 = tf.constant(basis(lvls, 0), dtype=tf.complex128) psi_actual = tf.matmul(U, psi_0) return populations(psi_actual, lindbladian=False)[lvl]
[docs]def lindbladian_population(propagators: dict, lvl: int, gate: str): U = propagators[gate] lvls = int(np.sqrt(U.shape[0])) psi_0 = tf.constant(basis(lvls, 0), dtype=tf.complex128) dv_0 = tf_dm_to_vec(tf_state_to_dm(psi_0)) dv_actual = tf.matmul(U, dv_0) return populations(dv_actual, lindbladian=True)[lvl]
[docs]@fid_reg_deco def RB( propagators, min_length: int = 5, max_length: int = 500, num_lengths: int = 20, num_seqs: int = 30, logspace=False, lindbladian=False, padding="", ): gate = list(propagators.keys())[0] U = propagators[gate] dim = int(U.shape[0]) psi_init = tf.constant(basis(dim, 0), dtype=tf.complex128) if logspace: lengths = np.rint( np.logspace(np.log10(min_length), np.log10(max_length), num=num_lengths) ).astype(int) else: lengths = np.rint(np.linspace(min_length, max_length, num=num_lengths)).astype( int ) surv_prob = [] for L in lengths: seqs = single_length_RB(num_seqs, L, padding) Us = evaluate_sequences(propagators, seqs) pop0s = [] for U in Us: pops = populations(tf.matmul(U, psi_init), lindbladian) pop0s.append(float(pops[0])) surv_prob.append(pop0s) def RB_fit(len, r, A, B): return A * r ** (len) + B bounds = (0, 1) init_guess = [0.9, 0.5, 0.5] fitted = False while not fitted: try: means = np.mean(surv_prob, axis=1) stds = np.std(surv_prob, axis=1) / np.sqrt(len(surv_prob[0])) solution, cov = curve_fit( RB_fit, lengths, means, sigma=stds, bounds=bounds, p0=init_guess ) r, A, B = solution fitted = True except Exception as message: print(message) if logspace: new_lengths = np.rint( np.logspace( np.log10(max_length + min_length), np.log10(max_length * 2), num=num_lengths, ) ).astype(int) else: new_lengths = np.rint( np.linspace( max_length + min_length, max_length * 2, num=num_lengths ) ).astype(int) max_length = max_length * 2 for L in new_lengths: seqs = single_length_RB(num_seqs, L, padding) Us = evaluate_sequences(propagators, seqs) pop0s = [] for U in Us: pops = populations(tf.matmul(U, psi_init), lindbladian) pop0s.append(float(pops[0])) surv_prob.append(pop0s) lengths = np.append(lengths, new_lengths) epc = 0.5 * (1 - r) epg = 1 - ((1 - epc) ** (1 / 4)) # TODO: adjust to be mean length of return epg
[docs]@fid_reg_deco def lindbladian_RB_left( propagators: dict, gate: str, index, dims, proj: bool = False, ): return RB(propagators, padding="left")
[docs]@fid_reg_deco def lindbladian_RB_right(propagators: dict, gate: str, index, dims, proj: bool): return RB(propagators, padding="right")
[docs]@fid_reg_deco def leakage_RB( propagators, min_length: int = 5, max_length: int = 500, num_lengths: int = 20, num_seqs: int = 30, logspace=False, lindbladian=False, ): gate = list(propagators.keys())[0] U = propagators[gate] dim = int(U.shape[0]) psi_init = tf.constant(basis(dim, 0), dtype=tf.complex128) if logspace: lengths = np.rint( np.logspace(np.log10(min_length), np.log10(max_length), num=num_lengths) ).astype(int) else: lengths = np.rint(np.linspace(min_length, max_length, num=num_lengths)).astype( int ) comp_surv = [] surv_prob = [] for L in lengths: seqs = single_length_RB(num_seqs, L) Us = evaluate_sequences(propagators, seqs) pop0s = [] pop_comps = [] for U in Us: pops = populations(tf.matmul(U, psi_init), lindbladian) pop0s.append(float(pops[0])) pop_comps.append(float(pops[0]) + float(pops[1])) surv_prob.append(pop0s) comp_surv.append(pop_comps) def RB_leakage(len, r_leak, A_leak, B_leak): return A_leak + B_leak * r_leak ** (len) bounds = (0, 1) init_guess = [0.9, 0.5, 0.5] fitted = False while not fitted: try: comp_means = np.mean(comp_surv, axis=1) comp_stds = np.std(comp_surv, axis=1) / np.sqrt(len(comp_surv[0])) solution, cov = curve_fit( RB_leakage, lengths, comp_means, sigma=comp_stds, bounds=bounds, p0=init_guess, ) r_leak, A_leak, B_leak = solution fitted = True except Exception as message: print(message) if logspace: new_lengths = np.rint( np.logspace( np.log10(max_length + min_length), np.log10(max_length * 2), num=num_lengths, ) ).astype(int) else: new_lengths = np.rint( np.linspace( max_length + min_length, max_length * 2, num=num_lengths ) ).astype(int) max_length = max_length * 2 for L in new_lengths: seqs = single_length_RB(num_seqs, L) Us = evaluate_sequences(propagators, seqs) pop0s = [] pop_comps = [] for U in Us: pops = populations(tf.matmul(U, psi_init), lindbladian) pop0s.append(float(pops[0])) pop_comps.append(float(pops[0])) surv_prob.append(pop0s) comp_surv.append(pop_comps) lengths = np.append(lengths, new_lengths) def RB_surv(len, r, A, C): return A + B_leak * r_leak ** (len) + C * r ** (len) bounds = (0, 1) init_guess = [0.9, 0.5, 0.5] fitted = False while not fitted: try: surv_means = np.mean(surv_prob, axis=1) surv_stds = np.std(surv_prob, axis=1) / np.sqrt(len(surv_prob[0])) solution, cov = curve_fit( RB_surv, lengths, surv_means, sigma=surv_stds, bounds=bounds, p0=init_guess, ) r, A, C = solution fitted = True except Exception as message: print(message) if logspace: new_lengths = np.rint( np.logspace( np.log10(max_length + min_length), np.log10(max_length * 2), num=num_lengths, ) ).astype(int) else: new_lengths = np.rint( np.linspace( max_length + min_length, max_length * 2, num=num_lengths ) ).astype(int) max_length = max_length * 2 for L in new_lengths: seqs = single_length_RB(num_seqs, L) Us = evaluate_sequences(propagators, seqs) pop0s = [] pop_comps = [] for U in Us: pops = populations(tf.matmul(U, psi_init), lindbladian) pop0s.append(float(pops[0])) pop_comps.append(float(pops[0])) surv_prob.append(pop0s) comp_surv.append(pop_comps) lengths = np.append(lengths, new_lengths) leakage = (1 - A_leak) * (1 - r_leak) seepage = A_leak * (1 - r_leak) fid = 0.5 * (r + 1 - leakage) epc = 1 - fid return epc, leakage, seepage, r_leak, A_leak, B_leak, r, A, C
[docs]@fid_reg_deco def orbit_infid( propagators, RB_number: int = 30, RB_length: int = 20, lindbladian=False, shots: int = None, seqs=None, noise=None, ): if not seqs: seqs = single_length_RB(RB_number=RB_number, RB_length=RB_length) Us = evaluate_sequences(propagators, seqs) infids = [] for U in Us: dim = int(U.shape[0]) psi_init = tf.constant(basis(dim, 0), dtype=tf.complex128) psi_actual = tf.matmul(U, psi_init) pop0 = tf_abs(psi_actual[0]) ** 2 p1 = 1 - pop0 if shots: vals = tf.keras.backend.random_binomial( [shots], p=p1, dtype=tf.float64, ) # if noise: # vals = vals + (np.random.randn(shots) * noise) infid = tf.reduce_mean(vals) else: infid = p1 # if noise: # infid = infid + (np.random.randn() * noise) if noise: infid = infid + (np.random.randn() * noise) infids.append(infid) return tf_ave(infids)
[docs]@fid_reg_deco def state_transfer_from_states(states: tf.Tensor, index, dims, params, n_eval=-1): infids = [] psi_0 = params["target"] if len(states.shape) > 2: overlap = calculate_state_overlap(states[-1], psi_0) else: overlap = calculate_state_overlap(states, psi_0) infid = 1 - overlap infids.append(infid) return tf.reduce_max(tf.math.real(infids))
[docs]def calculate_state_overlap(psi1, psi2): if psi1.shape[0] == psi1.shape[1]: return ( tf.linalg.trace( tf.sqrt(tf.matmul(tf.matmul(tf.sqrt(psi1), psi2), tf.sqrt(psi1))) ) ) ** 2 else: return tf_ketket_fid(psi1, psi2)