def test_qc(): from pyquil.api import ForestConnection, QuantumComputer from pyquil.api._compiler import _extract_attribute_dictionary_from_program from pyquil.api._qac import AbstractCompiler from pyquil.device import NxDevice from pyquil.gates import I class BasicQVMCompiler(AbstractCompiler): def quil_to_native_quil(self, program: Program): return basic_compile(program) def native_quil_to_executable(self, nq_program: Program): return PyQuilExecutableResponse( program=nq_program.out(), attributes=_extract_attribute_dictionary_from_program(nq_program)) try: qc = QuantumComputer( name='testing-qc', qam=QVM(connection=ForestConnection(), random_seed=52), device=NxDevice(nx.complete_graph(2)), compiler=BasicQVMCompiler(), ) qc.run_and_measure(Program(I(0)), trials=1) return qc except (RequestException, TimeoutError) as e: return pytest.skip("This test requires a running local QVM: {}".format(e))
def test_qpu_run(): config = PyquilConfig() if config.qpu_url and config.qpu_compiler_url: g = nx.Graph() g.add_node(0) device = NxDevice(g) qc = QuantumComputer( name="pyQuil test QC", qam=QPU(endpoint=config.qpu_url, user="******"), device=device, compiler=QPUCompiler( quilc_endpoint=config.quilc_url, qpu_compiler_endpoint=config.qpu_compiler_url, device=device, ), ) bitstrings = qc.run_and_measure(program=Program(X(0)), trials=1000) assert bitstrings[0].shape == (1000, ) assert np.mean(bitstrings[0]) > 0.8 bitstrings = qc.run(qc.compile(Program(X(0)))) assert bitstrings.shape == (0, 0) else: pytest.skip( "QPU or compiler-server not available; skipping QPU run test.")
def test_set_initial_state(): # That is test the assigned state matrix in ReferenceDensitySimulator is persistent between # rounds of run. rho1 = np.array([[0.0, 0.0], [0.0, 1.0]]) # run prog prog = Program(I(0)) ro = prog.declare("ro", "BIT", 1) prog += MEASURE(0, ro[0]) # make a quantum computer object device = NxDevice(nx.complete_graph(1)) qc_density = QuantumComputer( name="testy!", qam=PyQVM(n_qubits=1, quantum_simulator_type=ReferenceDensitySimulator), device=device, compiler=DummyCompiler(), ) qc_density.qam.wf_simulator.set_initial_state(rho1).reset() out = [qc_density.run(prog) for _ in range(0, 4)] ans = [np.array([[1]]), np.array([[1]]), np.array([[1]]), np.array([[1]])] assert all([np.allclose(x, y) for x, y in zip(out, ans)]) # Run and measure style progRAM = Program(I(0)) results = qc_density.run_and_measure(progRAM, trials=10) ans = {0: np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])} assert np.allclose(results[0], ans[0]) # test reverting ReferenceDensitySimulator to the default state rho0 = np.array([[1.0, 0.0], [0.0, 0.0]]) qc_density.qam.wf_simulator.set_initial_state(rho0).reset() assert np.allclose(qc_density.qam.wf_simulator.density, rho0) assert np.allclose(qc_density.qam.wf_simulator.initial_density, rho0)
topology = nx.from_edgelist([ (10, 2), (10, 4), (10, 6), (10, 8), ]) device = NxDevice(topology) class MyLazyCompiler(AbstractCompiler): def quil_to_native_quil(self, program, *, protoquil=None): return program def native_quil_to_executable(self, nq_program): return nq_program my_qc = QuantumComputer( name='my-qvm', qam=QVM(connection=ForestConnection()), device=device, compiler=MyLazyCompiler(), ) nx.draw(my_qc.qubit_topology()) plt.title('5qcm', fontsize=18) plt.show() my_qc.run_and_measure(Program(X(10)), trials=5)
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, )