def generate_t2_star_experiments( qubits: Sequence[int], times: Sequence[float], detuning: float = 1e6) -> List[ObservablesExperiment]: """ Return ObservablesExperiments containing programs which constitute a T2 star experiment to measure the T2 star coherence decay time for each qubit in qubits. For each delay time in times a single program will be generated in which all qubits are initialized to the minusY state and simultaneously measured along the Y axis after the given delay and Z rotation. If the qubit frequency is perfectly calibrated then the Y expectation will oscillate at the given detuning frequency as the qubit is rotated about the Z axis (with respect to the lab frame, which by hypothesis matches the natural qubit frame). :param qubits: list of qubits to measure. :param times: the times at which to measure, given in seconds. Each time is rounded to the nearest .1 microseconds. :param detuning: The additional detuning frequency about the z axis in Hz. :return: ObservablesExperiments which can be run to acquire an estimate of T2* for each qubit """ expts = [] for t in times: t = round(t, 7) # enforce 100ns boundaries program = Program() settings = [] for q in qubits: program += Pragma('DELAY', [q], str(t)) program += RZ(2 * pi * t * detuning, q) settings.append(ExperimentSetting(minusY(q), PauliTerm('Y', q))) expts.append(ObservablesExperiment([settings], program)) return expts
def test_survival_statistics(): # setup p0 = 0.98 # p(q0 = 0) p1 = 0.5 # p(q1 = 0) p_joint = p0 * p1 + (1 - p0) * (1 - p1) expectations = [2 * p0 - 1, 2 * p1 - 1, 2 * p_joint - 1] variances = np.asarray( [p0 * (1 - p0), p1 * (1 - p1), p_joint * (1 - p_joint)]) * 2**2 n = 10000 qubits = (0, 1) settings = [ ExperimentSetting(zeros_state(qubits), op) for op in all_traceless_pauli_z_terms(qubits) ] results = (ExperimentResult(setting, exp, n, std_err=np.sqrt(v / n)) for setting, exp, v in zip(settings, expectations, variances)) stats = get_stats_by_qubit_group([qubits], [results])[qubits] exps = stats['expectation'][0] errs = stats['std_err'][0] np.testing.assert_allclose(exps, expectations) np.testing.assert_allclose(errs, np.sqrt(variances / n)) survival_prob, survival_var = z_obs_stats_to_survival_statistics( exps, errs, n) np.testing.assert_allclose(survival_prob, p0 * p1) np.testing.assert_allclose(np.sqrt(survival_var), .004999, rtol=1e-4)
def generate_monte_carlo_state_dfe_experiment(benchmarker: BenchmarkConnection, program: Program, qubits: List[int], n_terms=200) \ -> ObservablesExperiment: """ Estimate state fidelity by sampled direct fidelity estimation. This leads to constant overhead (w.r.t. number of qubits) fidelity estimation. The algorithm is due to [DFE1]_ and [DFE2]_. :param program: A program comprised of clifford gates that constructs a state for which we estimate the fidelity. :param qubits: The qubits to perform DFE on. This can be a superset of the qubits used in ``program``, in which case it is assumed the identity acts on these qubits. Note that we assume qubits are initialized to the ``|0>`` state. :param benchmarker: The `BenchmarkConnection` object used to design experiments :param n_terms: Number of randomly chosen observables to measure. This number should be a constant less than ``2**len(qubits)``, otherwise ``exhaustive_state_dfe`` is more efficient. :return: an ObservablesExperiment that constitutes a state DFE experiment. """ # pick n_terms different random combinations of I and Z on the qubits rand_iz_paulis = np.random.choice(['I', 'Z'], size=(n_terms, len(qubits))) settings = [] for iz_pauli in rand_iz_paulis: # sample a new state if this one is all identity while 'Z' not in iz_pauli: iz_pauli = np.random.choice(['I', 'Z'], size=len(qubits)) # conjugate the non-trivial iz Pauli by the ideal state prep program obs = benchmarker.apply_clifford_to_pauli( program, str_to_pauli_term(''.join(iz_pauli))) settings.append(ExperimentSetting(zeros_state(qubits), obs)) return ObservablesExperiment(settings, program=program)
def test_survival_statistics_2(): # p0 is probability qubit 0 is 0 for p0 in [1.0, .99]: exp = 2 * p0 - 1 variance = p0 * (1 - p0) * 2**2 n = 100 qubits = (0, ) setting = [ ExperimentSetting(zeros_state(qubits), op) for op in all_traceless_pauli_z_terms(qubits) ][0] results = (ExperimentResult(setting, exp, n, std_err=np.sqrt(variance / n)), ) stats = get_stats_by_qubit_group([qubits], [results])[qubits] exps = stats['expectation'][0] errs = stats['std_err'][0] np.testing.assert_allclose(exps, [exp]) np.testing.assert_allclose(errs, [np.sqrt(variance / n)]) survival_prob, survival_var = z_obs_stats_to_survival_statistics( exps, errs) assert survival_prob == p0
def generate_exhaustive_state_dfe_experiment( benchmarker: BenchmarkConnection, program: Program, qubits: list) -> ObservablesExperiment: """ Estimate state fidelity by exhaustive direct fidelity estimation. This leads to a quadratic reduction in overhead w.r.t. state tomography for fidelity estimation. The algorithm is due to [DFE1]_ and [DFE2]_. :param benchmarker: object returned from pyquil.api.get_benchmarker() used to conjugate each Pauli by the Clifford program :param program: A program comprised of Clifford group gates that constructs a state for which we estimate the fidelity. :param qubits: The qubits to perform DFE on. This can be a superset of the qubits used in ``program``, in which case it is assumed the identity acts on these qubits. Note that we assume qubits are initialized to the ``|0>`` state. :return: an ObservablesExperiment that constitutes a state DFE experiment. """ # measure all of the traceless combinations of I and Z on the qubits conjugated by the ideal # Clifford state preparation program. The in_state is all the all zero state since this is # the assumed initialization of the state preparation. settings = [ ExperimentSetting(in_state=zeros_state(qubits), observable=benchmarker.apply_clifford_to_pauli( program, iz_pauli)) for iz_pauli in all_traceless_pauli_z_terms(qubits) ] return ObservablesExperiment(settings, program=program)
def pick_two_eigenvecs_prep_meas_settings( fix_qubit: Tuple[int, int], rotate_qubit: int, change_of_basis: Optional[Program] = None): prep_prog = Program() if change_of_basis is not None: prep_prog += change_of_basis if fix_qubit[1] == 1: fixed_q_state = minusZ( fix_qubit[0]) # initialize fixed qubit to |1> state else: fixed_q_state = plusZ( fix_qubit[0]) # initialize fixed qubit to |0> state init_state = fixed_q_state * plusX(rotate_qubit) fixed_q_ops = [PauliTerm('I', fix_qubit[0]), PauliTerm('Z', fix_qubit[0])] rot_q_ops = [PauliTerm('X', rotate_qubit), PauliTerm('Y', rotate_qubit)] settings = [ ExperimentSetting(init_state, term1 * term2) for term1 in fixed_q_ops for term2 in rot_q_ops ] # prepare superposition, return to z basis, then do the measurement settings. return prep_prog, Program(prep_prog).dagger(), settings
def test_survival_statistics_3(): # p0 is probability qubit 0 is 0 # p1 is probability qubit 1 is 0 for p0, p1 in zip([1.0, .99, .99], [1.0, 1.0, .99]): # setup p_joint = p0 * p1 + (1 - p0) * (1 - p1) expectations = [2 * p0 - 1, 2 * p1 - 1, 2 * p_joint - 1] variances = np.asarray( [p0 * (1 - p0), p1 * (1 - p1), p_joint * (1 - p_joint)]) * 2**2 n = 100 qubits = (0, 1) settings = [ ExperimentSetting(zeros_state(qubits), op) for op in all_traceless_pauli_z_terms(qubits) ] results = (ExperimentResult(setting, exp, n, std_err=np.sqrt( v / n)) for setting, exp, v in zip(settings, expectations, variances)) stats = get_stats_by_qubit_group([qubits], [results])[qubits] exps = stats['expectation'][0] errs = stats['std_err'][0] np.testing.assert_allclose(exps, expectations) np.testing.assert_allclose(errs, np.sqrt(variances / n)) survival_prob, survival_var = z_obs_stats_to_survival_statistics( exps, errs, n) assert survival_prob == p0 * p1
def generate_t1_experiments(qubits: Sequence[int], times: Sequence[float]) \ -> List[ObservablesExperiment]: """ Return a ObservablesExperiment containing programs which constitute a t1 experiment to measure the decay time from the excited state to ground state for each qubit in qubits. For each delay time in times a single program will be generated in which all qubits are initialized to the excited state (`|1>`) and simultaneously measured after the given delay. :param qubits: list of qubits to measure. :param times: The times at which to measure, given in seconds. Each time is rounded to the nearest .1 microseconds. :return: ObservablesExperiments which will measure the decay of each qubit after initialization to the 1 state and delay of t seconds for each t in times. """ expts = [] for t in times: t = round(t, 7) # enforce 100ns boundaries program = Program() settings = [] for q in qubits: program += Pragma('DELAY', [q], str(t)) settings.append(ExperimentSetting(minusZ(q), PauliTerm('Z', q))) expts.append(ObservablesExperiment([settings], program)) return expts
def generate_cz_phase_ramsey_experiments( cz_qubits: Sequence[int], measure_qubit: int, angles: Sequence[float]) -> List[ObservablesExperiment]: """ Return ObservablesExperiments containing programs that constitute a CZ phase ramsey experiment. :param cz_qubits: the qubits participating in the cz gate :param measure_qubit: Which qubit to measure. :param angles: A list of angles at which to make a measurement :return: ObservablesExperiments which can be run to estimate the effective RZ rotation applied to a single qubit during the application of a CZ gate. """ expts = [] for angle in angles: settings = [] program = Program() # apply CZ, possibly inducing an effective RZ on measure qubit by some angle program += CZ(*cz_qubits) # apply phase to measure_qubit akin to T2 experiment program += RZ(angle, measure_qubit) settings = [ ExperimentSetting(minusY(measure_qubit), PauliTerm('Y', measure_qubit)) ] expts.append(ObservablesExperiment([settings], program)) return expts
def test_R_operator_fixed_point_2_qubit(): # Check fixed point of operator. See Eq. 5 in Řeháček et al., PRA 75, 042108 (2007). qubits = [0, 1] id_setting = ExperimentSetting(in_state=zeros_state(qubits), observable=sI(qubits[0])*sI( qubits[1])) zz_setting = ExperimentSetting(in_state=zeros_state(qubits), observable=sZ(qubits[0])*sI( qubits[1])) id_result = ExperimentResult(setting=id_setting, expectation=1, total_counts=1) zzplus_result = ExperimentResult(setting=zz_setting, expectation=1, total_counts=1) zz_results = [id_result, zzplus_result] # Z basis test r = _R(P00, zz_results, qubits) actual = r @ P00 @ r np.testing.assert_allclose(actual, P00, atol=1e-12)
def generate_exhaustive_process_dfe_experiment( benchmarker: BenchmarkConnection, program: Program, qubits: list) -> ObservablesExperiment: """ Estimate process fidelity by exhaustive direct fidelity estimation (DFE). This leads to a quadratic reduction in overhead w.r.t. process tomography for fidelity estimation. The algorithm is due to: .. [DFE1] Practical Characterization of Quantum Devices without Tomography. Silva et al. PRL 107, 210404 (2011). https://doi.org/10.1103/PhysRevLett.107.210404 https://arxiv.org/abs/1104.3835 .. [DFE2] Direct Fidelity Estimation from Few Pauli Measurements. Flammia and Liu. PRL 106, 230501 (2011). https://doi.org/10.1103/PhysRevLett.106.230501 https://arxiv.org/abs/1104.4695 :param benchmarker: object returned from pyquil.api.get_benchmarker() used to conjugate each Pauli by the Clifford program :param program: A program comprised of Clifford group gates that defines the process for which we estimate the fidelity. :param qubits: The qubits to perform DFE on. This can be a superset of the qubits used in ``program``, in which case it is assumed the identity acts on these qubits. Note that we assume qubits are initialized to the ``|0>`` state. :return: an ObservablesExperiment that constitutes a process DFE experiment. """ settings = [] # generate all n-qubit pauli strings but skip the first all identity term for pauli_labels in [ ''.join(x) for x in itertools.product('IXYZ', repeat=len(qubits)) ][1:]: # calculate the appropriate output pauli from applying the ideal program to the Pauli observable = benchmarker.apply_clifford_to_pauli( program, str_to_pauli_term(pauli_labels, qubits)) # keep track of non-identity terms that may have a sign contribution non_identity_idx = [0 if label == 'I' else 1 for label in pauli_labels] # now replace the identities with Z terms, so they can be decomposed into Z eigenstates state_labels = [ 'Z' if label == 'I' else label for label in pauli_labels ] # loop over the ±1 eigenstates of each Pauli for eigenstate in itertools.product([0, 1], repeat=len(qubits)): in_state = TensorProductState( _OneQState(l, s, q) for l, s, q in zip(state_labels, eigenstate, qubits)) # make the observable negative if the in_state is a negative eigenstate sign_contribution = (-1)**np.dot(eigenstate, non_identity_idx) settings.append( ExperimentSetting(in_state=in_state, observable=observable * sign_contribution)) return ObservablesExperiment(settings, program=program)
def generate_monte_carlo_process_dfe_experiment(benchmarker: BenchmarkConnection, program: Program, qubits: List[int], n_terms: int = 200) \ -> ObservablesExperiment: """ Estimate process fidelity by randomly sampled direct fidelity estimation. This leads to constant overhead (w.r.t. number of qubits) fidelity estimation. The algorithm is due to [DFE1]_ and [DFE2]_. :param program: A program comprised of Clifford group gates that constructs a state for which we estimate the fidelity. :param qubits: The qubits to perform DFE on. This can be a superset of the qubits used in ``program``, in which case it is assumed the identity acts on these qubits. Note that we assume qubits are initialized to the ``|0>`` state. :param benchmarker: The `BenchmarkConnection` object used to design experiments :param n_terms: Number of randomly chosen observables to measure. This number should be a constant less than ``2**len(qubits)``, otherwise ``exhaustive_process_dfe`` is more efficient. :return: an ObservablesExperiment that constitutes a process DFE experiment. """ single_q_paulis = ['I', 'X', 'Y', 'Z'] # pick n_terms different random combinations of I, X, Y, Z on the qubits rand_paulis = np.random.randint(len(single_q_paulis), size=(n_terms, len(qubits))) settings = [] for pauli_idxs in rand_paulis: # sample a new state if this one is all identity while sum(pauli_idxs) == 0: pauli_idxs = np.random.randint(len(single_q_paulis), size=len(qubits)) # convert from indices to string pauli_str = ''.join([single_q_paulis[idx] for idx in pauli_idxs]) # convert from string to PauliTerm on appropriate qubits pauli = str_to_pauli_term(pauli_str, qubits) # calculate the appropriate output pauli from applying the ideal program to the Pauli observable = benchmarker.apply_clifford_to_pauli(program, pauli) # now replace the identities with Z terms, so they can be decomposed into Z eigenstates state_labels = ['Z' if label == 'I' else label for label in pauli_str] # randomly pick between ±1 eigenstates of each Pauli eigenstate = np.random.randint(2, size=len(qubits)) in_state = TensorProductState( _OneQState(l, s, q) for l, s, q in zip(state_labels, eigenstate, qubits)) # make the observable negative if the in_state is a negative eigenstate # only keep track of minus eigenstates associated to non-identity Paulis, i.e. idx >= 1 sign_contribution = (-1)**np.dot(eigenstate, [min(1, idx) for idx in pauli_idxs]) settings.append( ExperimentSetting(in_state=in_state, observable=observable * sign_contribution)) return ObservablesExperiment(settings, program=program)
def _state_tomo_settings(qubits: Sequence[int]): """ Yield settings over every non-identity observable in the Pauli basis over the qubits. Used as a helper function for generate_state_tomography_experiment :param qubits: The qubits to tomographize. """ for obs in all_traceless_pauli_terms(qubits): yield ExperimentSetting( in_state=zeros_state(qubits), observable=obs, )
def _sic_process_tomo_settings(qubits: Sequence[int]): """ Yield settings over SIC basis across I, X, Y, Z operators Used as a helper function for generate_process_tomography_experiment :param qubits: The qubits to tomographize. """ for in_sics in itertools.product([SIC0, SIC1, SIC2, SIC3], repeat=len(qubits)): i_state = functools.reduce(mul, (state(q) for state, q in zip(in_sics, qubits)), TensorProductState()) for obs in all_traceless_pauli_terms(qubits): yield ExperimentSetting( in_state=i_state, observable=obs, )
def group_sequences_into_parallel_experiments(parallel_expts_seqs: Sequence[List[List[Program]]], qubit_groups: Sequence[Sequence[int]], is_unitarity_expt: bool = False) \ -> List[ObservablesExperiment]: """ Consolidates randomized benchmarking sequences on separate groups of qubits into a flat list of ObservablesExperiments which merge parallel sets of distinct sequences. Each returned ObservablesExperiment constitutes a single 'parallel RB sequence' where all of the qubits are acted upon and measured. Running all of these ObservablesExperiments in series constitutes a 'parallel RB' experiment from which you can determine a decay constant for each group of qubits. Note that there is an important physical distinction (e.g. due to cross-talk) between running separate RB experiments on different groups of qubits and running a 'parallel RB' experiment on the collection of those groups. For this reason one should not expect in general that the rb decay for a particular group of qubits is comparable between the individual and parallel modes of rb experiment. :param parallel_expts_seqs: the outer Sequence is indexed by disjoint groups of qubits; Clifford sequences from each of these different groups (which should be of the same depth across qubit groups) will be merged together into a single program. The intended use-case is that each List[List[program]] of sequences of Cliffords is an output of generate_rb_experiment_sequences for disjoint groups of qubits but with identical depths input (see generate_rb_experiments for example). If sequences of different depth are merged into a Program then some qubits may be sitting idle while the sequences of greater depth continue running. Measurement occurs only when all sequences have terminated. :param qubit_groups: The partition of the qubits into groups for each of which you would like to estimate an rb decay. Typically this grouping of qubits should match the qubits that are acted on by each sequence in the corresponding List[List[Program]] of the input parallel_expts_seqs. :param is_unitarity_expt: True if the desired experiment is a unitarity experiment, in which case additional settings are required to estimate the purity of the sequence output. :return: a list of ObservablesExperiments constituting a parallel rb experiment. """ expts = [] for parallel_sequence_group in zip(*parallel_expts_seqs): program = merge_programs(merge_sequences(parallel_sequence_group)) if is_unitarity_expt: settings = [sett for group in qubit_groups for sett in _state_tomo_settings(group)] expt = group_settings(ObservablesExperiment(settings, program)) else: # measure observables of products of I and Z on qubits in the group, excluding all I settings = [ExperimentSetting(zeros_state(group), op) for group in qubit_groups for op in all_traceless_pauli_z_terms(group)] expt = ObservablesExperiment([settings], program) expts.append(expt) return expts
def _pauli_process_tomo_settings(qubits): """ Yield settings over +-XYZ basis across I, X, Y, Z operators Used as a helper function for generate_process_tomography_experiment :param qubits: The qubits to tomographize. """ for states in itertools.product([plusX, minusX, plusY, minusY, plusZ, minusZ], repeat=len(qubits)): i_state = functools.reduce(mul, (state(q) for state, q in zip(states, qubits)), TensorProductState()) for obs in all_traceless_pauli_terms(qubits): yield ExperimentSetting( in_state=i_state, observable=obs, )
def generate_t2_echo_experiments( qubits: Sequence[int], times: Sequence[float], detuning: float = 1e6) -> List[ObservablesExperiment]: """ Return ObservablesExperiments containing programs which constitute a T2 echo experiment to measure the T2 echo coherence decay time. For each delay time in times a single program will be generated in which all qubits are initialized to the minusY state and later simultaneously measured along the Y axis. Unlike in the t2_star experiment above there is a 'echo' applied in the middle of the delay in which the qubit is rotated by pi radians around the Y axis. Similarly to t2_star, if the qubit frequency is perfectly calibrated then the Y expectation will oscillate at the given detuning frequency as the qubit is rotated about the Z axis (with respect to the lab frame, which by hypothesis matches the natural qubit frame). Unlike in a t2_star experiment, even if the qubit frequency is off such that there is some spurious rotation about the Z axis during the DELAY, the effect of an ideal echo is to cancel the effect of this rotation so that the qubit returns to the initial state minusY before the detuning rotation is applied. :param qubits: list of qubits to measure. :param times: the times at which to measure, given in seconds. Each time is rounded to the nearest .1 microseconds. :param detuning: The additional detuning frequency about the z axis. :return: ObservablesExperiments which can be run to acquire an estimate of T2 for each qubit. """ expts = [] for t in times: half_time = round(t / 2, 7) # enforce 100ns boundaries t = round(t, 7) program = Program() settings = [] for q in qubits: half_delay = Pragma('DELAY', [q], str(half_time)) # echo program += [half_delay, RY(pi, q), half_delay] # apply detuning program += RZ(2 * pi * t * detuning, q) settings.append(ExperimentSetting(minusY(q), PauliTerm('Y', q))) expts.append(ObservablesExperiment(settings, program)) return expts
def all_eigenvector_prep_meas_settings(qubits: Sequence[int], change_of_basis: Program): # experiment settings put initial state in superposition of computational basis # the prep and pre_meas programs simply convert between this and superposition of eigenvectors prep_prog = Program(change_of_basis) pre_meas_prog = Program(change_of_basis).dagger() init_state = reduce(mul, [plusX(q) for q in qubits], TensorProductState()) settings = [] for xy_q in qubits: z_qubits = [q for q in qubits if q != xy_q] xy_terms = [PauliTerm('X', xy_q), PauliTerm('Y', xy_q)] iz_terms = [PauliTerm('I', xy_q)] iz_terms += [PauliTerm('Z', q) for q in z_qubits] settings += [ ExperimentSetting(init_state, xy_term * term) for xy_term in xy_terms for term in iz_terms ] return prep_prog, pre_meas_prog, settings
def generate_rabi_experiments(qubits: Sequence[int], angles: Sequence[float]) \ -> List[ObservablesExperiment]: """ Return ObservablesExperiments containing programs which constitute a Rabi experiment. Rabi oscillations are observed by applying successively larger rotations to the same initial state. :param qubits: list of qubits to measure. :param angles: A list of angles at which to make a measurement :return: ObservablesExperiments which can be run to verify the RX(angle, q) calibration for each qubit """ expts = [] for angle in angles: program = Program() settings = [] for q in qubits: program += RX(angle, q) settings.append(ExperimentSetting(plusZ(q), PauliTerm('Z', q))) expts.append(ObservablesExperiment([settings], program)) return expts
ExperimentSetting, zeros_state, calibrate_observable_estimates from pyquil.paulis import sI, sZ, sX from pyquil.quil import Program from forest.benchmarking import distance_measures as dm # Single qubit defs Q0 = 0 PROJ_ZERO = np.array([[1, 0], [0, 0]]) PROJ_ONE = np.array([[0, 0], [0, 1]]) ID = PROJ_ZERO + PROJ_ONE PLUS = np.array([[1], [1]]) / np.sqrt(2) PROJ_PLUS = PLUS @ PLUS.T.conj() PROJ_MINUS = ID - PROJ_PLUS ID_SETTING = ExperimentSetting(in_state=zeros_state([Q0]), observable=sI(Q0)) Z_SETTING = ExperimentSetting(in_state=zeros_state([Q0]), observable=sZ(Q0)) X_SETTING = ExperimentSetting(in_state=zeros_state([Q0]), observable=sX(Q0)) # Two qubit defs P00 = np.kron(PROJ_ZERO, PROJ_ZERO) P01 = np.kron(PROJ_ZERO, PROJ_ONE) P10 = np.kron(PROJ_ONE, PROJ_ZERO) P11 = np.kron(PROJ_ONE, PROJ_ONE) def test_generate_1q_state_tomography_experiment(): qubits = [0] prog = Program(I(qubits[0])) one_q_exp = generate_state_tomography_experiment(prog, qubits=qubits) dimension = 2 ** len(qubits)