def test_readout_symmetrization(forest): device = NxDevice(nx.complete_graph(3)) noise_model = decoherence_noise_with_asymmetric_ro(gates=gates_in_isa(device.get_isa())) qc = QuantumComputer( name='testy!', qam=QVM(connection=forest, noise_model=noise_model), device=device, compiler=DummyCompiler() ) prog = Program(I(0), X(1), MEASURE(0, 0), MEASURE(1, 1)) prog.wrap_in_numshots_loop(1000) bs1 = qc.run(prog) avg0_us = np.mean(bs1[:, 0]) avg1_us = 1 - np.mean(bs1[:, 1]) diff_us = avg1_us - avg0_us assert diff_us > 0.03 bs2 = qc.run_symmetrized_readout(prog, 1000) avg0_s = np.mean(bs2[:, 0]) avg1_s = 1 - np.mean(bs2[:, 1]) diff_s = avg1_s - avg0_s assert diff_s < 0.05
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 calibrate_observable_estimates(qc: QuantumComputer, expt_results: List[ExperimentResult], num_shots: int = 500, symm_type: int = -1, noisy_program: Program = None, active_reset: bool = False, show_progress_bar: bool = False) \ -> Iterable[ExperimentResult]: """ Calibrates the expectation and std_err of the input expt_results and updates those estimates. The input expt_results should be estimated with symmetrized readout error for this to work properly. Calibration is done by measuring expectation values of eigenstates of the observable, which ideally should yield either +/- 1 but in practice will have magnitude less than 1. For default exhaustive_symmetrization the calibration expectation magnitude averaged over all eigenvectors is recorded as calibration_expectation. The original expectation is moved to raw_expectation and replaced with the old value scaled by the inverse calibration expectation. :param qc: a quantum computer object on which to run the programs necessary to calibrate each result. :param expt_results: a list of results, each of which will be separately calibrated. :param num_shots: the number of shots to run for each eigenvector :param symm_type: the type of symmetrization * -1 -- exhaustive symmetrization uses every possible combination of flips; this option is the default since it ensures proper calibration and is only exponential in the weight of each observable (rather than the total number of qubits in a program). * 0 -- no symmetrization * 1 -- symmetrization using an OA with strength 1 * 2 -- symmetrization using an OA with strength 2 * 3 -- symmetrization using an OA with strength 3 TODO: accomodate calibration for weight > symmetrization strength (symm_type) Currently, the symmetrization type must be at least the maximum weight of any observable estimated and also match the symmetrization type used to estimate the observables. :param noisy_program: an optional program from which to inherit a noise model; only relevant for running on a QVM :param active_reset: whether or not to begin the program by actively resetting. If true, execution of each of the returned programs in a loop on the QPU will generally be faster. :param show_progress_bar: displays a progress bar via tqdm if true. :return: a copy of the input results with updated estimates and calibration results. """ observables = [copy(res.setting.observable) for res in expt_results] for obs in observables: obs.coefficient = complex(1.) observables = list(set(observables)) # get unique observables that will need to be calibrated programs = [get_calibration_program(obs, noisy_program, active_reset) for obs in observables] meas_qubits = [obs.get_qubits() for obs in observables] calibrations = {} for prog, meas_qs, obs in zip(tqdm(programs, disable=not show_progress_bar), meas_qubits, observables): results = qc.run_symmetrized_readout(prog, num_shots, symm_type, meas_qs) # Obtain statistics from result of experiment obs_mean, obs_var = shots_to_obs_moments(results, meas_qs, obs) calibrations[obs.operations_as_set()] = (obs_mean, obs_var, len(results)) for expt_result in expt_results: # TODO: allow weight > symm_type if -1 < symm_type < len(expt_result.setting.observable.get_qubits()): warnings.warn(f'Calibration of observable {expt_result.setting.observable} ' f'currently not supported since it acts on more qubits than the ' f'symm_type {symm_type}.') # get the calibration data for this observable cal_data = calibrations[expt_result.setting.observable.operations_as_set()] obs_mean, obs_var, counts = cal_data # Use the calibration to correct the mean and var result_mean = expt_result.expectation result_var = expt_result.std_err**2 corrected_mean = result_mean / obs_mean corrected_var = ratio_variance(result_mean, result_var, obs_mean, obs_var) yield ExperimentResult( setting=expt_result.setting, expectation=corrected_mean, std_err=np.sqrt(corrected_var), total_counts=expt_result.total_counts, raw_expectation=result_mean, raw_std_err=expt_result.std_err, calibration_expectation=obs_mean, calibration_std_err=np.sqrt(obs_var), calibration_counts=counts )
def estimate_observables(qc: QuantumComputer, obs_expt: ObservablesExperiment, num_shots: int = 500, symm_type: int = 0, active_reset: bool = False, show_progress_bar: bool = False, use_basic_compile: bool = True)\ -> Iterable[ExperimentResult]: """ Standard wrapper for estimating the observables in an `ObservablesExperiment`. An expectation and standard error will be estimated for each observable in each setting of the ObservablesExperiment. Settings which are grouped together will be run in parallel under the same call to the QuantumComputer. .. CAUTION:: Note that the call to `qc.run_symmetrized_readout` adds MEASURE instructions to the programs output by :func:`generate_experiment_programs` and also uses the qc.compiler to compile each program. The default compiler (e.g. for qc returned by `get_qc`) may make optimizations that remove gates necessary for benchmarking. If this is not desired, consider setting the use_basic_compile flag to True for a basic compilation pass that simply replaces gates with non-native gates. :param qc: a quantum computer object on which to run the programs necessary to estimate each observable of obs_expt. :param obs_expt: a single ObservablesExperiment with settings pre-grouped as desired. :param num_shots: the number of shots to run each program or each symmetrized program. :param symm_type: the type of symmetrization * -1 -- exhaustive symmetrization uses every possible combination of flips * 0 -- no symmetrization * 1 -- symmetrization using an OA with strength 1 * 2 -- symmetrization using an OA with strength 2 * 3 -- symmetrization using an OA with strength 3 :param active_reset: whether or not to begin the program by actively resetting. If true, execution of each of the returned programs in a loop on the QPU will generally be faster. :param show_progress_bar: displays a progress bar via tqdm if true. :param use_basic_compile: instead of using the qc.compiler standard quil_to_native_quil compilation step, which may optimize gates away, instead use only basic_compile which makes as few manual gate substitutions as possible. :return: all of the ExperimentResults which hold an estimate of each observable of obs_expt """ if use_basic_compile: old_method = qc.compiler.quil_to_native_quil # temporarily replace compiler.quil_to_native_quil with basic_compile qc.compiler.quil_to_native_quil = basic_compile programs, meas_qubits = generate_experiment_programs(obs_expt, active_reset) for prog, meas_qs, settings in zip(tqdm(programs, disable=not show_progress_bar), meas_qubits, obs_expt): results = qc.run_symmetrized_readout(prog, num_shots, symm_type, meas_qs) for setting in settings: observable = setting.observable # Obtain statistics from result of experiment obs_mean, obs_var = shots_to_obs_moments(results, meas_qs, observable) yield ExperimentResult( setting=setting, expectation=obs_mean, std_err=np.sqrt(obs_var), total_counts=len(results), ) if use_basic_compile: # revert to original qc.compiler.quil_to_native_quil = old_method