def test_is_identity(): pt1 = -1.5j * sI(2) pt2 = 1.5 * sX(1) * sZ(2) assert is_identity(pt1) assert is_identity(pt2 + (-1 * pt2) + sI(0)) assert not is_identity(0 * pt1) assert not is_identity(pt2 + (-1 * pt2))
def _commutes(p1, p2): # Identity commutes with anything if is_identity(p1) or is_identity(p2): return True # Operators acting on different qubits commute if len(set(p1.get_qubits()) & set(p2.get_qubits())) == 0: return True # Otherwise, they must be the same thing modulo coefficient return p1.id() == p2.id()
def test_check_commutation_rigorous(): # more rigorous test. Get all operators in Pauli group p_n_group = ("I", "X", "Y", "Z") pauli_list = list(product(p_n_group, repeat=3)) pauli_ops = [list(zip(x, range(3))) for x in pauli_list] pauli_ops_pq = [reduce(mul, (PauliTerm(*x) for x in op)) for op in pauli_ops] non_commuting_pairs = [] commuting_pairs = [] for x in range(len(pauli_ops_pq)): for y in range(x, len(pauli_ops_pq)): tmp_op = _commutator(pauli_ops_pq[x], pauli_ops_pq[y]) assert len(tmp_op.terms) == 1 if is_identity(tmp_op.terms[0]): commuting_pairs.append((pauli_ops_pq[x], pauli_ops_pq[y])) else: non_commuting_pairs.append((pauli_ops_pq[x], pauli_ops_pq[y])) # now that we have our sets let's check against our code. for t1, t2 in non_commuting_pairs: assert not check_commutation([t1], t2) for t1, t2 in commuting_pairs: assert check_commutation([t1], t2)
def apply_clifford_to_pauli(self, clifford, pauli_in): r""" Given a circuit that consists only of elements of the Clifford group, return its action on a PauliTerm. In particular, for Clifford C, and Pauli P, this returns the PauliTerm representing CPC^{\dagger}. :param Program clifford: A Program that consists only of Clifford operations. :param PauliTerm pauli_in: A PauliTerm to be acted on by clifford via conjugation. :return: A PauliTerm corresponding to clifford * pauli_in * clifford^{\dagger} """ # do nothing if `pauli_in` is the identity if is_identity(pauli_in): return pauli_in indices_and_terms = list(zip(*list(pauli_in.operations_as_set()))) payload = ConjugateByCliffordRequest( clifford=clifford.out(), pauli=rpcq.messages.PauliTerm(indices=list(indices_and_terms[0]), symbols=list(indices_and_terms[1]))) response: ConjugateByCliffordResponse = self.client.call( 'conjugate_pauli_by_clifford', payload) phase_factor, paulis = response.phase, response.pauli pauli_out = PauliTerm("I", 0, 1.j**phase_factor) clifford_qubits = clifford.get_qubits() pauli_qubits = pauli_in.get_qubits() all_qubits = sorted(set(pauli_qubits).union(set(clifford_qubits))) # The returned pauli will have specified its value on all_qubits, sorted by index. # This is maximal set of qubits that can be affected by this conjugation. for i, pauli in enumerate(paulis): pauli_out *= PauliTerm(pauli, all_qubits[i]) return pauli_out * pauli_in.coefficient
def _pauli_to_product_state(in_state: PauliTerm) -> TensorProductState: """ Convert a Pauli term to a TensorProductState. """ if is_identity(in_state): return TensorProductState() else: return TensorProductState([ _OneQState(label=pauli_label, index=0, qubit=cast(int, qubit)) for qubit, pauli_label in in_state._ops.items() ])
def remove_identity(psum): """ Remove the identity term from a Pauli sum :param psum: :return: """ new_psum = [] identity_terms = [] for term in psum: if not is_identity(term): new_psum.append(term) else: identity_terms.append(term) return sum(new_psum), sum(identity_terms)
def remove_identity(psum): """ Remove the identity term from a Pauli sum :param PauliSum psum: PauliSum object to remove identity :return: The new pauli sum and the identity term. """ new_psum = [] identity_terms = [] for term in psum: if not is_identity(term): new_psum.append(term) else: identity_terms.append(term) return sum(new_psum), sum(identity_terms)
def __init__(self, in_state: TensorProductState, out_operator: PauliTerm): # For backwards compatibility, handle in_state specified by PauliTerm. if isinstance(in_state, PauliTerm): warnings.warn("Please specify in_state as a TensorProductState", DeprecationWarning, stacklevel=2) if is_identity(in_state): in_state = TensorProductState() else: in_state = TensorProductState([ _OneQState(label=pauli_label, index=0, qubit=qubit) for qubit, pauli_label in in_state._ops.items() ]) object.__setattr__(self, 'in_state', in_state) object.__setattr__(self, 'out_operator', out_operator)
def remove_identity( psum: PauliSum ) -> Tuple[Union[PauliSum, PauliTerm], Union[float, PauliSum, PauliTerm]]: """ Remove the identity term from a Pauli sum. :param psum: The PauliSum to process. :return: The sums of the non-identity and identity PauliSums. """ new_psum = [] identity_terms = [] for term in psum: if not is_identity(term): new_psum.append(term) else: identity_terms.append(term) return sum(new_psum), sum(identity_terms)
def _sic_process_tomo_settings(qubits: Sequence[int]): """Yield settings over SIC basis cross 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 o_ops in itertools.product([sI, sX, sY, sZ], repeat=len(qubits)): o_op = functools.reduce(mul, (op(q) for op, q in zip(o_ops, qubits)), sI()) if is_identity(o_op): continue yield ExperimentSetting( in_state=i_state, out_operator=o_op, )
def _pauli_process_tomo_settings(qubits): """Yield settings over +-XYZ basis cross 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 o_ops in itertools.product([sI, sX, sY, sZ], repeat=len(qubits)): o_op = functools.reduce(mul, (op(q) for op, q in zip(o_ops, qubits)), sI()) if is_identity(o_op): continue yield ExperimentSetting( in_state=i_state, out_operator=o_op, )
def measure_observables(qc: QuantumComputer, tomo_experiment: TomographyExperiment, n_shots: int = 10000, progress_callback=None, active_reset=False, symmetrize_readout: str = 'exhaustive', calibrate_readout: str = 'plus-eig'): """ Measure all the observables in a TomographyExperiment. :param qc: A QuantumComputer which can run quantum programs :param tomo_experiment: A suite of tomographic observables to measure :param n_shots: The number of shots to take per ExperimentSetting :param progress_callback: If not None, this function is called each time a group of settings is run with arguments ``f(i, len(tomo_experiment)`` such that the progress is ``i / len(tomo_experiment)``. :param active_reset: Whether to actively reset qubits instead of waiting several times the coherence length for qubits to decay to |0> naturally. Setting this to True is much faster but there is a ~1% error per qubit in the reset operation. Thermal noise from "traditional" reset is not routinely characterized but is of the same order. :param symmetrize_readout: Method used to symmetrize the readout errors, i.e. set p(0|1) = p(1|0). For uncorrelated readout errors, this can be achieved by randomly selecting between the POVMs {X.D1.X, X.D0.X} and {D0, D1} (where both D0 and D1 are diagonal). However, here we currently support exhaustive symmetrization and loop through all possible 2^n POVMs {X/I . POVM . X/I}^n, and obtain symmetrization more generally, i.e. set p(00|00) = p(01|01) = .. = p(11|11), as well as p(00|01) = p(01|00) etc. If this is None, no symmetrization is performed. The exhaustive method can be specified by setting this variable to 'exhaustive' (default value). Set to `None` if no symmetrization is desired. :param calibrate_readout: Method used to calibrate the readout results. Currently, the only method supported is normalizing against the operator's expectation value in its +1 eigenstate, which can be specified by setting this variable to 'plus-eig' (default value). The preceding symmetrization and this step together yield a more accurate estimation of the observable. Set to `None` if no calibration is desired. """ # calibration readout only works with symmetrization turned on if calibrate_readout is not None and symmetrize_readout is None: raise ValueError("Readout calibration only works with readout symmetrization turned on") # Outer loop over a collection of grouped settings for which we can simultaneously # estimate. for i, settings in enumerate(tomo_experiment): log.info(f"Collecting bitstrings for the {len(settings)} settings: {settings}") # 1.1 Prepare a state according to the amalgam of all setting.in_state total_prog = Program() if active_reset: total_prog += RESET() max_weight_in_state = _max_weight_state(setting.in_state for setting in settings) for oneq_state in max_weight_in_state.states: total_prog += _one_q_state_prep(oneq_state) # 1.2 Add in the program total_prog += tomo_experiment.program # 1.3 Measure the state according to setting.out_operator max_weight_out_op = _max_weight_operator(setting.out_operator for setting in settings) for qubit, op_str in max_weight_out_op: total_prog += _local_pauli_eig_meas(op_str, qubit) # 2. Symmetrization qubits = max_weight_out_op.get_qubits() if symmetrize_readout == 'exhaustive' and len(qubits) > 0: bitstrings, d_qub_idx = _exhaustive_symmetrization(qc, qubits, n_shots, total_prog) elif symmetrize_readout is None and len(qubits) > 0: total_prog_no_symm = total_prog.copy() ro = total_prog_no_symm.declare('ro', 'BIT', len(qubits)) d_qub_idx = {} for i, q in enumerate(qubits): total_prog_no_symm += MEASURE(q, ro[i]) # Keep track of qubit-classical register mapping via dict d_qub_idx[q] = i total_prog_no_symm.wrap_in_numshots_loop(n_shots) total_prog_no_symm_native = qc.compiler.quil_to_native_quil(total_prog_no_symm) total_prog_no_symm_bin = qc.compiler.native_quil_to_executable(total_prog_no_symm_native) bitstrings = qc.run(total_prog_no_symm_bin) elif len(qubits) == 0: # looks like an identity operation pass else: raise ValueError("Readout symmetrization method must be either 'exhaustive' or None") if progress_callback is not None: progress_callback(i, len(tomo_experiment)) # 3. Post-process # Inner loop over the grouped settings. They only differ in which qubits' measurements # we include in the post-processing. For example, if `settings` is Z1, Z2, Z1Z2 and we # measure (n_shots, n_qubits=2) obs_strings then the full operator value involves selecting # either the first column, second column, or both and multiplying along the row. for setting in settings: # 3.1 Get the term's coefficient so we can multiply it in later. coeff = complex(setting.out_operator.coefficient) if not np.isclose(coeff.imag, 0): raise ValueError(f"{setting}'s out_operator has a complex coefficient.") coeff = coeff.real # 3.2 Special case for measuring the "identity" operator, which doesn't make much # sense but should happen perfectly. if is_identity(setting.out_operator): yield ExperimentResult( setting=setting, expectation=coeff, std_err=0.0, total_counts=n_shots, ) continue # 3.3 Obtain statistics from result of experiment obs_mean, obs_var = _stats_from_measurements(bitstrings, d_qub_idx, setting, n_shots, coeff) if calibrate_readout == 'plus-eig': # 4 Readout calibration # 4.1 Obtain calibration program calibr_prog = _calibration_program(qc, tomo_experiment, setting) # 4.2 Perform symmetrization on the calibration program if symmetrize_readout == 'exhaustive': qubs_calibr = setting.out_operator.get_qubits() calibr_shots = n_shots calibr_results, d_calibr_qub_idx = _exhaustive_symmetrization(qc, qubs_calibr, calibr_shots, calibr_prog) else: raise ValueError("Readout symmetrization method must be either 'exhaustive' or None") # 4.3 Obtain statistics from the measurement process obs_calibr_mean, obs_calibr_var = _stats_from_measurements(calibr_results, d_calibr_qub_idx, setting, calibr_shots) # 4.3 Calibrate the readout results corrected_mean = obs_mean / obs_calibr_mean corrected_var = ratio_variance(obs_mean, obs_var, obs_calibr_mean, obs_calibr_var) yield ExperimentResult( setting=setting, expectation=corrected_mean.item(), std_err=np.sqrt(corrected_var).item(), total_counts=n_shots, raw_expectation=obs_mean.item(), raw_std_err=np.sqrt(obs_var).item(), calibration_expectation=obs_calibr_mean.item(), calibration_std_err=np.sqrt(obs_calibr_var).item(), calibration_counts=calibr_shots, ) elif calibrate_readout is None: # No calibration yield ExperimentResult( setting=setting, expectation=obs_mean.item(), std_err=np.sqrt(obs_var).item(), total_counts=n_shots, ) else: raise ValueError("Calibration readout method must be either 'plus-eig' or None")
def measure_observables( qc: QuantumComputer, tomo_experiment: Experiment, n_shots: Optional[int] = None, progress_callback: Optional[Callable[[int, int], None]] = None, active_reset: Optional[bool] = None, symmetrize_readout: Optional[Union[int, str]] = "None", calibrate_readout: Optional[str] = "plus-eig", readout_symmetrize: Optional[str] = None, ) -> Generator[ExperimentResult, None, None]: """ Measure all the observables in a TomographyExperiment. :param qc: A QuantumComputer which can run quantum programs :param tomo_experiment: A suite of tomographic observables to measure :param progress_callback: If not None, this function is called each time a group of settings is run with arguments ``f(i, len(tomo_experiment)`` such that the progress is ``i / len(tomo_experiment)``. :param calibrate_readout: Method used to calibrate the readout results. Currently, the only method supported is normalizing against the operator's expectation value in its +1 eigenstate, which can be specified by setting this variable to 'plus-eig' (default value). The preceding symmetrization and this step together yield a more accurate estimation of the observable. Set to `None` if no calibration is desired. """ shots = tomo_experiment.shots symmetrization = tomo_experiment.symmetrization reset = tomo_experiment.reset if n_shots is not None: warnings.warn( "'n_shots' has been deprecated; if you want to set the number of shots " "for this run of measure_observables please provide the number to " "Program.wrap_in_numshots_loop() for the Quil program that you provide " "when creating your TomographyExperiment object. For now, this value will " "override that in the TomographyExperiment, but eventually this keyword " "argument will be removed.", FutureWarning, ) shots = n_shots else: if shots == 1: warnings.warn( "'n_shots' has been deprecated; if you want to set the number of shots " "for this run of measure_observables please provide the number to " "Program.wrap_in_numshots_loop() for the Quil program that you provide " "when creating your TomographyExperiment object. It looks like your " "TomographyExperiment object has shots = 1, so for now we will change " "that to 10000, which was the previous default value.", FutureWarning, ) shots = 10000 if active_reset is not None: warnings.warn( "'active_reset' has been deprecated; if you want to enable active qubit " "reset please provide a Quil program that has a RESET instruction in it when " "creating your TomographyExperiment object. For now, this value will " "override that in the TomographyExperiment, but eventually this keyword " "argument will be removed.", FutureWarning, ) reset = active_reset if readout_symmetrize is not None and symmetrize_readout != "None": raise ValueError( "'readout_symmetrize' and 'symmetrize_readout' are conflicting keyword " "arguments -- please provide only one.") if readout_symmetrize is not None: warnings.warn( "'readout_symmetrize' has been deprecated; please provide the symmetrization " "level when creating your TomographyExperiment object. For now, this value " "will override that in the TomographyExperiment, but eventually this keyword " "argument will be removed.", FutureWarning, ) symmetrization = SymmetrizationLevel(readout_symmetrize) if symmetrize_readout != "None": warnings.warn( "'symmetrize_readout' has been deprecated; please provide the symmetrization " "level when creating your TomographyExperiment object. For now, this value " "will override that in the TomographyExperiment, but eventually this keyword " "argument will be removed.", FutureWarning, ) if symmetrize_readout is None: symmetrize_readout = SymmetrizationLevel.NONE elif symmetrize_readout == "exhaustive": symmetrize_readout = SymmetrizationLevel.EXHAUSTIVE symmetrization = SymmetrizationLevel(symmetrize_readout) # calibration readout only works with symmetrization turned on if calibrate_readout is not None and symmetrization != SymmetrizationLevel.EXHAUSTIVE: raise ValueError( "Readout calibration only currently works with exhaustive readout " "symmetrization turned on.") # generate programs for each group of simultaneous settings. programs, meas_qubits = _generate_experiment_programs( tomo_experiment, reset) for i, (prog, qubits, settings) in enumerate(zip(programs, meas_qubits, tomo_experiment)): log.info( f"Collecting bitstrings for the {len(settings)} settings: {settings}" ) # we don't need to do any actual measurement if the combined operator is simply the # identity, i.e. weight=0. We handle this specially below. if len(qubits) > 0: # obtain (optionally symmetrized) bitstring results for all of the qubits bitstrings = qc.run_symmetrized_readout(prog, shots, symmetrization, qubits) if progress_callback is not None: progress_callback(i, len(tomo_experiment)) # Post-process # Inner loop over the grouped settings. They only differ in which qubits' measurements # we include in the post-processing. For example, if `settings` is Z1, Z2, Z1Z2 and we # measure (shots, n_qubits=2) obs_strings then the full operator value involves selecting # either the first column, second column, or both and multiplying along the row. for setting in settings: # Get the term's coefficient so we can multiply it in later. coeff = setting.out_operator.coefficient assert isinstance(coeff, Complex) if not np.isclose(coeff.imag, 0): raise ValueError( f"{setting}'s out_operator has a complex coefficient.") coeff = coeff.real # Special case for measuring the "identity" operator, which doesn't make much # sense but should happen perfectly. if is_identity(setting.out_operator): yield ExperimentResult(setting=setting, expectation=coeff, std_err=0.0, total_counts=shots) continue # Obtain statistics from result of experiment obs_mean, obs_var = _stats_from_measurements( bitstrings, {q: idx for idx, q in enumerate(qubits)}, setting, shots, coeff) if calibrate_readout == "plus-eig": # Readout calibration # Obtain calibration program calibr_prog = _calibration_program(qc, tomo_experiment, setting) calibr_qubs = setting.out_operator.get_qubits() calibr_qub_dict = { cast(int, q): idx for idx, q in enumerate(calibr_qubs) } # Perform symmetrization on the calibration program calibr_results = qc.run_symmetrized_readout( calibr_prog, shots, SymmetrizationLevel.EXHAUSTIVE, calibr_qubs) # Obtain statistics from the measurement process obs_calibr_mean, obs_calibr_var = _stats_from_measurements( calibr_results, calibr_qub_dict, setting, shots) # Calibrate the readout results corrected_mean = obs_mean / obs_calibr_mean corrected_var = ratio_variance(obs_mean, obs_var, obs_calibr_mean, obs_calibr_var) yield ExperimentResult( setting=setting, expectation=corrected_mean.item(), std_err=np.sqrt(corrected_var).item(), total_counts=len(bitstrings), raw_expectation=obs_mean.item(), raw_std_err=np.sqrt(obs_var).item(), calibration_expectation=obs_calibr_mean.item(), calibration_std_err=np.sqrt(obs_calibr_var).item(), calibration_counts=len(calibr_results), ) elif calibrate_readout is None: # No calibration yield ExperimentResult( setting=setting, expectation=obs_mean.item(), std_err=np.sqrt(obs_var).item(), total_counts=len(bitstrings), ) else: raise ValueError( "Calibration readout method must be either 'plus-eig' or None" )
def test_identity_no_qubit(): assert is_identity(sI())
def measure_observables(qc: QuantumComputer, tomo_experiment: TomographyExperiment, n_shots: int = 1000, progress_callback=None, active_reset=False, readout_symmetrize: str = None, calibrate_readout: str = None): """ Measure all the observables in a TomographyExperiment. :param qc: A QuantumComputer which can run quantum programs :param tomo_experiment: A suite of tomographic observables to measure :param n_shots: The number of shots to take per ExperimentSetting :param progress_callback: If not None, this function is called each time a group of settings is run with arguments ``f(i, len(tomo_experiment)`` such that the progress is ``i / len(tomo_experiment)``. :param active_reset: Whether to actively reset qubits instead of waiting several times the coherence length for qubits to decay to |0> naturally. Setting this to True is much faster but there is a ~1% error per qubit in the reset operation. Thermal noise from "traditional" reset is not routinely characterized but is of the same order. :param readout_symmetrize: Method used to symmetrize the readout errors, i.e. set p(0|1) = p(1|0). For uncorrelated readout errors, this can be achieved by randomly selecting between the POVMs {X.D1.X, X.D0.X} and {D0, D1} (where both D0 and D1 are diagonal). However, here we currently support exhaustive symmetrization and loop through all possible 2^n POVMs {X/I . POVM . X/I}^n, and obtain symmetrization more generally, i.e. set p(00|00) = p(01|01) = .. = p(11|11), as well as p(00|01) = p(01|00) etc. If this is None, no symmetrization is performed. The exhaustive method can be specified by setting this variable to 'exhaustive' :param calibrate_readout: Method used to calibrate the readout results. Currently, the only method supported is normalizing against the operator's expectation value in its +1 eigenstate, which can be specified by setting this variable to 'plus-eig'. The preceding symmetrization and this step together yield a more accurate estimation of the observable. """ # calibration readout only works with symmetrization turned on if calibrate_readout is not None and readout_symmetrize is None: raise ValueError( "Readout calibration only works with readout symmetrization turned on" ) # Outer loop over a collection of grouped settings for which we can simultaneously # estimate. for i, settings in enumerate(tomo_experiment): log.info( f"Collecting bitstrings for the {len(settings)} settings: {settings}" ) # 1.1 Prepare a state according to the amalgam of all setting.in_state total_prog = Program() if active_reset: total_prog += RESET() max_weight_in_state = _max_weight_state(setting.in_state for setting in settings) for oneq_state in max_weight_in_state.states: total_prog += _one_q_state_prep(oneq_state) # 1.2 Add in the program total_prog += tomo_experiment.program # 1.3 Measure the state according to setting.out_operator max_weight_out_op = _max_weight_operator(setting.out_operator for setting in settings) for qubit, op_str in max_weight_out_op: total_prog += _local_pauli_eig_meas(op_str, qubit) if readout_symmetrize == 'exhaustive': # 1.4 Symmetrize -- flip qubits pre-measurement qubits = max_weight_out_op.get_qubits() n_shots_symm = n_shots // 2**len(qubits) list_bitstrings_symm = [] for ops_bool in itertools.product([0, 1], repeat=len(qubits)): total_prog_symm = total_prog.copy() prog_symm = _ops_bool_to_prog(ops_bool, qubits) total_prog_symm += prog_symm # 2. Run the experiment bitstrings_symm = qc.run_and_measure(total_prog_symm, n_shots_symm) # 2.1 Flip the results post-measurement d_flips_symm = { qubits[i]: op_bool for i, op_bool in enumerate(ops_bool) } for qubit, bs_results in bitstrings_symm.items(): bitstrings_symm[qubit] = bs_results ^ d_flips_symm.get( qubit, 0) # 2.2 Gather together the symmetrized results into list list_bitstrings_symm.append(bitstrings_symm) # 2.3 Gather together all the symmetrized results bitstrings = reduce(_stack_dicts, list_bitstrings_symm) elif readout_symmetrize is None: # 2. Run the experiment bitstrings = qc.run_and_measure(total_prog, n_shots) else: raise ValueError( "Readout symmetrization method must be either 'exhaustive' or None" ) if progress_callback is not None: progress_callback(i, len(tomo_experiment)) # 3. Post-process # Inner loop over the grouped settings. They only differ in which qubits' measurements # we include in the post-processing. For example, if `settings` is Z1, Z2, Z1Z2 and we # measure (n_shots, n_qubits=2) obs_strings then the full operator value involves selecting # either the first column, second column, or both and multiplying along the row. for setting in settings: # 3.1 Get the term's coefficient so we can multiply it in later. coeff = complex(setting.out_operator.coefficient) if not np.isclose(coeff.imag, 0): raise ValueError( f"{setting}'s out_operator has a complex coefficient.") coeff = coeff.real # 3.2 Special case for measuring the "identity" operator, which doesn't make much # sense but should happen perfectly. if is_identity(setting.out_operator): yield ExperimentResult( setting=setting, expectation=coeff, stddev=0.0, total_counts=n_shots, ) continue # 3.3 Obtain statistics from result of experiment obs_mean, obs_var = _stats_from_measurements( bitstrings, setting, n_shots, coeff) if calibrate_readout == 'plus-eig': # 4 Readout calibration # 4.1 Prepare the +1 eigenstate for the out operator calibr_prog = Program() for q, op in setting.out_operator.operations_as_set(): calibr_prog += _one_q_pauli_prep(label=op, index=0, qubit=q) # 4.2 Measure the out operator in this state for q, op in setting.out_operator.operations_as_set(): calibr_prog += _local_pauli_eig_meas(op, q) calibr_shots = n_shots calibr_results = qc.run_and_measure(calibr_prog, calibr_shots) # 4.3 Obtain statistics from the measurement process obs_calibr_mean, obs_calibr_var = _stats_from_measurements( calibr_results, setting, calibr_shots) # 4.4 Calibrate the readout results corrected_mean = obs_mean / obs_calibr_mean corrected_var = ratio_variance(obs_mean, obs_var, obs_calibr_mean, obs_calibr_var) yield ExperimentResult( setting=setting, expectation=corrected_mean.item(), stddev=np.sqrt(corrected_var).item(), total_counts=n_shots, raw_expectation=obs_mean.item(), raw_stddev=np.sqrt(obs_var).item(), calibration_expectation=obs_calibr_mean.item(), calibration_stddev=np.sqrt(obs_calibr_var).item(), calibration_counts=calibr_shots, ) elif calibrate_readout is None: # No calibration yield ExperimentResult( setting=setting, expectation=obs_mean.item(), stddev=np.sqrt(obs_var).item(), total_counts=n_shots, ) else: raise ValueError( "Calibration readout method must be either 'plus-eig' or None" )
def measure_observables(qc: QuantumComputer, tomo_experiment: TomographyExperiment, n_shots=1000, progress_callback=None, active_reset=False): """ Measure all the observables in a TomographyExperiment. :param qc: A QuantumComputer which can run quantum programs :param tomo_experiment: A suite of tomographic observables to measure :param n_shots: The number of shots to take per ExperimentSetting :param progress_callback: If not None, this function is called each time a group of settings is run with arguments ``f(i, len(tomo_experiment)`` such that the progress is ``i / len(tomo_experiment)``. :param active_reset: Whether to actively reset qubits instead of waiting several times the coherence length for qubits to decay to |0> naturally. Setting this to True is much faster but there is a ~1% error per qubit in the reset operation. Thermal noise from "traditional" reset is not routinely characterized but is of the same order. """ for i, settings in enumerate(tomo_experiment): # Outer loop over a collection of grouped settings for which we can simultaneously # estimate. log.info( f"Collecting bitstrings for the {len(settings)} settings: {settings}" ) # 1.1 Prepare a state according to the amalgam of all setting.in_state total_prog = Program() if active_reset: total_prog += RESET() max_weight_in_state = _max_weight_state(setting.in_state for setting in settings) for oneq_state in max_weight_in_state.states: total_prog += _one_q_state_prep(oneq_state) # 1.2 Add in the program total_prog += tomo_experiment.program # 1.3 Measure the state according to setting.out_operator max_weight_out_op = _max_weight_operator(setting.out_operator for setting in settings) for qubit, op_str in max_weight_out_op: total_prog += _local_pauli_eig_meas(op_str, qubit) # 2. Run the experiment bitstrings = qc.run_and_measure(total_prog, n_shots) if progress_callback is not None: progress_callback(i, len(tomo_experiment)) # 3. Post-process # 3.1 First transform bits to eigenvalues; ie (+1, -1) obs_strings = {q: 1 - 2 * bitstrings[q] for q in bitstrings} # Inner loop over the grouped settings. They only differ in which qubits' measurements # we include in the post-processing. For example, if `settings` is Z1, Z2, Z1Z2 and we # measure (n_shots, n_qubits=2) obs_strings then the full operator value involves selecting # either the first column, second column, or both and multiplying along the row. for setting in settings: # 3.2 Get the term's coefficient so we can multiply it in later. coeff = complex(setting.out_operator.coefficient) if not np.isclose(coeff.imag, 0): raise ValueError( f"{setting}'s out_operator has a complex coefficient.") coeff = coeff.real # 3.3 Special case for measuring the "identity" operator, which doesn't make much # sense but should happen perfectly. if is_identity(setting.out_operator): yield ExperimentResult( setting=setting, expectation=coeff, stddev=0.0, total_counts=n_shots, ) continue # 3.4 Pick columns corresponding to qubits with a non-identity out_operation and stack # into an array of shape (n_shots, n_measure_qubits) my_obs_strings = np.vstack(obs_strings[q] for q, op_str in setting.out_operator).T # 3.6 Multiply row-wise to get operator values. Do statistics. Yield result. obs_vals = coeff * np.prod(my_obs_strings, axis=1) obs_mean = np.mean(obs_vals) obs_var = np.var(obs_vals) / n_shots yield ExperimentResult( setting=setting, expectation=obs_mean.item(), stddev=np.sqrt(obs_var).item(), total_counts=n_shots, )