def estimate_joint_confusion_in_set(qc: QuantumComputer, qubits: Sequence[int] = None, num_shots: int = 1000, joint_group_size: int = 1, use_param_program: bool = True, use_active_reset=False, show_progress_bar: bool = False) \ -> Dict[Tuple[int, ...], np.ndarray]: """ Measures the joint readout confusion matrix for all groups of size group_size among the qubits. Simultaneous readout of multiple qubits may exhibit correlations in readout error. Measuring a single qubit confusion for each qubit of interest is therefore not sufficient to completely characterize readout error in general. This method estimates joint readout confusion matrices for groups of qubits, which involves preparing all 2^joint_group_size possible bitstrings on each of (len(qubits) choose joint_group_size) many groups. The joint_group_size specifies which order of correlation you wish to characterize, and a separate confusion matrix is estimated for every possible group of that size. For example, a joint_group_size of one will simply estimate a single-qubit 2x2 confusion matrix for each of the provided qubits, similar to len(qubits) calls to estimate_confusion_matrix. Meanwhile, a joint_group_size of two will yield estimates of the (len(qubits) choose 2) many 4x4 confusion matrices for each possible pair of qubits. When the joint_group_size is the same as len(qubits) a single confusion matrix is estimated, which requires 2^len(qubits) number of state preparations. Note that the maximum number of measurements occurs when 1 << joint_group_size << len(qubits), and this maximum is quite large. Because this method performs the measurement of exponentially many bitstrings on a particular group of qubits, use_param_program should result in an appreciable speed up for estimation of each matrix by looping over bitstrings at a lower level of the stack, rather than creating a new program for each bitstring. Use_active_reset should also speed up estimation, at the cost of introducing more error and potentially complicating interpretation of results. :param qc: a quantum computer whose readout error you wish to characterize :param qubits: a list of accessible qubits on the qc you wish to characterize. Defaults to all qubits in qc. :param num_shots: number of shots in measurement of each bit string on each joint group of qubits. :param joint_group_size: the size of each group; a joint confusion matrix with 2^joint_group_size number of rows/columns will be estimated for each group of qubits of the given size within the qubit set. :param use_param_program: dictates whether to use a parameterized program to measure out each bitstring. If set to default of True, this routine should execute faster on a QPU. Note that the parameterized option does not execute a no-op when measuring 0. :param use_active_reset: if true, all qubits in qc (not just provided qubits) will be actively reset to 0 state at the start of each bitstring measurement. This option is intended as a potential speed up, but may complicate interpretation of the estimated confusion matrices. The method estimate_joint_active_reset_confusion separately characterizes active reset. :param show_progress_bar: displays a progress bar via tqdm if true. :return: a dictionary whose keys are all possible joint_group_sized tuples that can be formed from the qubits. Each value is an estimated 2^group_size square confusion matrix for the corresponding tuple of qubits. Each key is listed in order of increasing qubit number. The corresponding matrix has rows and columns indexed in increasing bitstring order, with most significant (leftmost) bit labeling the smallest qubit number. """ # establish default as all operational qubits in qc if qubits is None: qubits = qc.qubits() qubits = sorted(qubits) groups = list(itertools.combinations(qubits, joint_group_size)) confusion_matrices = {} total_num_rounds = len(groups) * 2**joint_group_size with tqdm(total=total_num_rounds, disable=not show_progress_bar) as pbar: for group in groups: # initialize program to optionally actively reset. Active reset will provide a speed up # but may make the estimate harder to interpret due to errors in the reset. program_start = Program() if use_active_reset: program_start += RESET() executable = None # will be re-assigned to either compiled param or non-param program # generate one parameterized program now, or later create a program for each bitstring if use_param_program: # generate parameterized program for this group and append to start param_program = program_start + _readout_group_parameterized_bitstring( group) param_program.wrap_in_numshots_loop(shots=num_shots) executable = qc.compiler.native_quil_to_executable( param_program) matrix = np.zeros((2**joint_group_size, 2**joint_group_size)) for row, bitstring in enumerate( itertools.product([0, 1], repeat=joint_group_size)): if use_param_program: # specify bitstring in parameterization at run-time results = qc.run( executable, memory_map={'target': [bit * pi for bit in bitstring]}) else: # generate program that measures given bitstring on group, and append to start bitstring_program = program_start + _readout_group_bitstring( group, bitstring) bitstring_program.wrap_in_numshots_loop(shots=num_shots) executable = qc.compiler.native_quil_to_executable( bitstring_program) results = qc.run(executable) # update confusion matrix for result in results: base = np.array( [2**i for i in reversed(range(joint_group_size))]) observed = np.sum(base * result) matrix[row, observed] += 1 / num_shots # update the progress bar pbar.update(1) # store completed confusion matrix in dictionary confusion_matrices[group] = matrix return confusion_matrices
def estimate_joint_reset_confusion(qc: QuantumComputer, qubits: Sequence[int] = None, num_trials: int = 10, joint_group_size: int = 1, use_active_reset: bool = True, show_progress_bar: bool = False) \ -> Dict[Tuple[int, ...], np.ndarray]: """ Measures a reset 'confusion matrix' for all groups of size joint_group_size among the qubits. Specifically, for each possible joint_group_sized group among the qubits we perform a measurement for each bitstring on that group. The measurement proceeds as follows: -Repeatedly try to prepare the bitstring state on the qubits until success. -If use_active_reset is true (default) actively reset qubits to ground state; otherwise, wait the preset amount of time for qubits to decay to ground state. -Measure the state after the chosen reset method. Since reset should result in the all zeros state this 'confusion matrix' should ideally have all ones down the left-most column of the matrix. The entry at (row_idx, 0) thus represents the success probability of reset given that the pre-reset state is the binary representation of the number row_idx. WARNING: this method can be very slow :param qc: a quantum computer whose reset error you wish to characterize :param qubits: a list of accessible qubits on the qc you wish to characterize. Defaults to all qubits in qc. :param num_trials: number of repeated trials of reset after preparation of each bitstring on each joint group of qubits. Note: num_trials does not correspond to num_shots; a new program must be run in each trial, so running a group of n trials takes longer than would collecting the same number of shots output from a single program. :param joint_group_size: the size of each group; a square matrix with 2^joint_group_size number of rows/columns will be estimated for each group of qubits of the given size among the provided qubits. :param use_active_reset: dictates whether to actively reset qubits or else wait the pre-allotted amount of time for the qubits to decay to the ground state. Using active reset will allow for faster data collection. :param show_progress_bar: displays a progress bar via tqdm if true. :return: a dictionary whose keys are all possible joint_group_sized tuples that can be formed from the qubits. Each value is an estimated 2^group_size square matrix for the corresponding tuple of qubits. Each key is listed in order of increasing qubit number. The corresponding matrix has rows and columns indexed in increasing bitstring order, with most significant (leftmost) bit labeling the smallest qubit number. Each matrix row corresponds to the bitstring that was prepared before the reset, and each column corresponds to the bitstring measured after the reset. """ # establish default as all operational qubits in qc if qubits is None: qubits = qc.qubits() qubits = sorted(qubits) groups = list(itertools.combinations(qubits, joint_group_size)) confusion_matrices = {} total_num_rounds = len(groups) * 2**joint_group_size with tqdm(total=total_num_rounds, disable=not show_progress_bar) as pbar: for group in groups: # program prepares a bit string (specified by parameterization) and immediately measures prep_program = _readout_group_parameterized_bitstring(group) prep_executable = qc.compiler.native_quil_to_executable( prep_program) matrix = np.zeros((2**joint_group_size, 2**joint_group_size)) for row, bitstring in enumerate( itertools.product([0, 1], repeat=joint_group_size)): for _ in range(num_trials): # try preparation at most 10 times. for _ in range(10): # prepare the given bitstring and measure result = qc.run(prep_executable, memory_map={ 'target': [bit * pi for bit in bitstring] }) # if the preparation is successful, move on to reset. if np.array_equal(result[0], bitstring): break # execute program that measures the post-reset state if use_active_reset: # program runs immediately after prep program and actively resets qubits reset_measure_program = Program(RESET()) else: # this program automatically waits pre-allotted time for qubits to decay reset_measure_program = Program() ro = reset_measure_program.declare('ro', memory_type='BIT', memory_size=len(qubits)) for idx, qubit in enumerate(group): reset_measure_program += MEASURE(qubit, ro[idx]) executable = qc.compiler.native_quil_to_executable( reset_measure_program) results = qc.run(executable) # update confusion matrix for result in results: base = np.array( [2**i for i in reversed(range(joint_group_size))]) observed = np.sum(base * result) matrix[row, observed] += 1 / num_trials # update the progress bar pbar.update(1) # store completed confusion matrix in dictionary confusion_matrices[group] = matrix return confusion_matrices
def run(qc: QuantumComputer, seq: List[Program], sg: List[Tuple], num_trials: int) -> np.ndarray: prog = rb_seq_to_program(seq, sg).wrap_in_numshots_loop(num_trials) executable = qc.compiler.native_quil_to_executable(prog) return qc.run(executable)
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 run(qc: QuantumComputer, exp: Program, n_trials: int) -> np.ndarray: exp.wrap_in_numshots_loop(n_trials) executable = qc.compiler.native_quil_to_executable(basic_compile(exp)) return qc.run(executable)
class ForestDiscreteEnv(gym.Env): """The Rigetti Forest environment. This implements a Gym environment for gate-based quantum computing with problem-specific rewards on the Rigetti hardware. Attributes: observation: A np.array, formed by concatenating observed bitstring values with a vector containing the problem weights. observation_space: The (continuous) set of possible observations. action space: The space of discrete actions. instrs: A table mapping action IDs to PyQuil gates. Args: data: A path to a numpy dataset. label: Either a path to a dataset of labels, or a single label value. shuffle: A flag indicating whether the data should be randomly shuffled. qpu: A flag indicating whether to run on the qpu given by QPU_NAME. """ def __init__(self, data, label, shuffle=False, qpu=False): weights = np.load(data) n_graphs = len(weights) # read labels from file, or as single label if os.path.exists(label): labels = np.load(label) else: labels = [label for _ in range(n_graphs)] if shuffle: self._shuffled_order = np.random.permutation(n_graphs) weights = weights[self._shuffled_order] labels = labels[self._shuffled_order] self.pset = AllProblems(weights, labels) self.num_qubits = self.pset.num_variables() qubits = list(range(self.num_qubits)) angles = np.linspace(0, 2 * np.pi, NUM_ANGLES, endpoint=False) self.instrs = [ CNOT(q0, q1) for q0, q1 in product(qubits, qubits) if q0 != q1 ] self.instrs += [ op(theta, q) for q, op, theta in product(qubits, [RX, RY, RZ], angles) ] self.action_space = gym.spaces.Discrete(len(self.instrs)) obs_len = NUM_SHOTS * self.num_qubits + len(self.pset.problem(0)) self.observation_space = gym.spaces.Box(np.full(obs_len, -1.0), np.full(obs_len, 1.0), dtype=np.float32) self.reward_threshold = 0.8 self.qpu = qpu if qpu: self._qc = get_qc(QPU_NAME) else: self._qc = QuantumComputer( name="qvm", qam=PyQVM(n_qubits=self.num_qubits), device=NxDevice(nx.complete_graph(self.num_qubits)), compiler=MinimalPyQVMCompiler(), ) self.reset() def reset(self, problem_id=None): """Reset the state of the environment. This clears out whatever program you may have assembled so far, and updates the active problem. Args: problem_id: The numeric index of the problem (relative to the problem set). If None, a random problem will be chosen. """ if problem_id is None: problem_id = np.random.randint(0, self.pset.num_problems()) self.problem_id = problem_id self._prob_vec = self.pset.problem(self.problem_id) # the scoring function (for reward computation) self._prob_score = self.pset.bitstrings_score(self.problem_id) # we put some trivial gates on each relevant qubit, so that we can # always recover the problem variables from the program itself self.program = Program([I(q) for q in range(self.num_qubits)]) self.current_step = 0 self.running_episode_reward = 0 self.bitstrings, info = self._run_program(self.program) return self.observation @property def observation(self): """Get the current observed quantum + problem state.""" # This consists of two things: # - the measured bitstrings # - the vectorized representation of the optimization problem # # In particular, the first 10*NUM_SHOTS (i.e. 100) entries are measured # qubit values. The remaining entries are the weights of the problem # graph. return np.concatenate([self.bitstrings.flatten(), self._prob_vec]) def step(self, action): """Advance the environment by performing the specified action.""" # get the instruction indicated by the action instr = self.instrs[action] # extend the program self.program.inst(instr) # run and get some measured bitstrings self.bitstrings, info = self._run_program(self.program) # compute the avg score of the bitstrings reward = self._prob_score(self.bitstrings) self.running_episode_reward += reward info["instr"] = instr info["reward-nb"] = reward self.current_step += 1 # are we done yet? done = False if self.current_step >= MAX_PROGRAM_LENGTH: done = True if reward >= self.reward_threshold: reward += MAX_PROGRAM_LENGTH - self.current_step done = True return self.observation, reward, done, info def _wrap_program(self, program): # the actions select gates. but a pyquil program needs a bit more # namely, declaration of classical memory for readout, and suitable # measurement instructions ro = program.declare("ro", "BIT", self.num_qubits) for q in range(self.num_qubits): program.inst(MEASURE(q, ro[q])) program.wrap_in_numshots_loop(NUM_SHOTS) return program def _run_program(self, program): program = program.copy() if self.qpu: # time to go through the compiler. whee! pragma = Program( [Pragma("INITIAL_REWIRING", ['"PARTIAL"']), RESET()]) program = pragma + program program = self._wrap_program(program) nq_program = self._qc.compiler.quil_to_native_quil(program) gate_count = sum(1 for instr in nq_program if isinstance(instr, Gate)) executable = self._qc.compiler.native_quil_to_executable( nq_program) results = self._qc.run(executable=executable) else: program = self._wrap_program(program) gate_count = len(program) results = self._qc.run(program) info = { "gate_count": gate_count } # compiled length for qpu, uncompiled for qvm return results, info def render(self, mode="human"): raise NotImplementedError( "Rendering of this environment not currently supported.") def seed(self, seed): np.random.seed(seed)
def run_program( program: Program, qc: QuantumComputer, ) -> np.ndarray: return qc.run(qc.compile(program)).readout_data.get('ro')
def test_basic_program(qc: QuantumComputer): results = qc.run(qc.compile(TEST_PROGRAM)).readout_data.get("ro") assert results.shape == (1000, 2)
def estimate_pauli_sum(pauli_terms: List[PauliTerm], basis_transform_dict: Dict[int, str], program: Program, variance_bound: float, quantum_resource: QuantumComputer, commutation_check: bool = True) -> EstimationResult: """ Estimate the mean of a sum of pauli terms to set variance. The sample variance is calculated by .. math:: \begin{align} \mathrm{Var}[\hat{\langle H \rangle}] = \sum_{i, j}h_{i}h_{j} \mathrm{Cov}(\hat{\langle P_{i} \rangle}, \hat{\langle P_{j} \rangle}) \end{align} The expectation value of each Pauli operator (term and coefficient) is also returned. It can be accessed through the named-tuple field `pauli_expectations'. :param pauli_terms: list of pauli terms to measure simultaneously or a PauliSum object :param basis_transform_dict: basis transform dictionary where the key is the qubit index and the value is the basis to rotate into. Valid basis is [I, X, Y, Z]. :param program: program generating a state to sample from. The program is deep copied to ensure no mutation of gates or program is perceived by the user. :param variance_bound: Bound on the variance of the estimator for the PauliSum. Remember this is the SQUARE of the standard error! :param quantum_resource: quantum abstract machine object :param commutation_check: Optional flag toggling a safety check ensuring all terms in `pauli_terms` commute with each other :return: estimated expected value, expected value of each Pauli term in the sum, covariance matrix, variance of the estimator, and the number of shots taken. The objected returned is a named tuple with field names as follows: expected_value, pauli_expectations, covariance, variance, n_shots. `expected_value' == coef_vec.dot(pauli_expectations) """ if not isinstance(pauli_terms, (list, PauliSum)): raise TypeError("pauli_terms needs to be a list or a PauliSum") if isinstance(pauli_terms, PauliSum): pauli_terms = pauli_terms.terms # check if each term commutes with everything if commutation_check: if len(commuting_sets(sum(pauli_terms))) != 1: raise CommutationError("Not all terms commute in the expected way") program = copy.deepcopy(program) pauli_for_rotations = PauliTerm.from_list([ (value, key) for key, value in basis_transform_dict.items() ]) program += get_rotation_program(pauli_for_rotations) qubits = sorted(list(basis_transform_dict.keys())) for qubit in qubits: program.inst(MEASURE(qubit, qubit)) coeff_vec = np.array(list(map(lambda x: x.coefficient, pauli_terms))).reshape((-1, 1)) # upper bound on samples given by IV of arXiv:1801.03524 num_sample_ubound = int( np.ceil(np.sum(np.abs(coeff_vec))**2 / variance_bound)) results = None sample_variance = np.infty number_of_samples = 0 while (sample_variance > variance_bound and number_of_samples < num_sample_ubound): ro = program.declare('ro', 'BIT', len(qubits)) program += [ MEASURE(qubit, ro_reg) for qubit, ro_reg in zip(qubits, ro) ] program.wrap_in_numshots_loop(min(10000, num_sample_ubound)) executable = quantum_resource.compiler.native_quil_to_executable( program) tresults = quantum_resource.run(executable) number_of_samples += len(tresults) parity_results = get_parity(pauli_terms, tresults) # Note: easy improvement would be to update mean and variance on the fly # instead of storing all these results. if results is None: results = parity_results else: results = np.hstack((results, parity_results)) # calculate the expected values.... covariance_mat = np.cov(results) sample_variance = coeff_vec.T.dot(covariance_mat).dot( coeff_vec) / results.shape[1] return EstimationResult( expected_value=coeff_vec.T.dot(np.mean(results, axis=1)), pauli_expectations=np.multiply(coeff_vec.flatten(), np.mean(results, axis=1).flatten()), covariance=covariance_mat, variance=sample_variance, n_shots=results.shape[1])
def __init__( self, list_gsuit_paulis: List[PauliTerm], qc: QuantumComputer, ref_state: Program, ansatz: Program, shotN: int, parametric_way: bool, n_qubits: int, active_reset: bool = True, cq=None, method='QC', verbose: bool = False, calibration: bool = True, ): """ A tomography experiment class for use in VQE. In a real experiment, one only has access to measurements in the Z-basis, giving a result 0 or 1 for each qubit. The Hamiltonian may have terms like sigma_x or sigma_y, for which a basis rotation is required. As quantum mechanics prohibits measurements in multiple bases at once, the Hamiltonian needs to be grouped into commuting Pauli terms and for each set of terms the appropriate basis rotations is applied to each qubits. A parity_matrix is constructed to keep track of what consequence a qubit measurement of 0 or 1 has as a contribution to the Pauli estimation. The experiments are constructed as objects with class methods to run and adjust them. Instantiate using the following parameters: :param list() list_gsuit_paulis: list of Pauli terms which can be measured at the same time (they share a TPB!) :param QuantumComputer qc: QuantumComputer() object which will simulate the terms :param Program ref_state: Program() circuit object which produces the initial reference state (f.ex. Hartree-Fock) :param Program ansatz: Program() circuit object which produces the ansatz (f.ex. UCCSD) :param int shotN: number of shots to run this Setting for :param bool parametric_way: boolean whether to use parametric gates or hard-coded gates :param int n_qubits: total number of qubits used for the program :param bool active_reset: boolean whether or not to actively reset the qubits :param list() cq: list of qubit labels instead of the default [0,1,2,3,...,N-1] :param str method: string describing the computational method from {QC, linalg, WFS, Numpy} :param bool verbose: boolean, whether or not to give verbose output during execution """ self.parametric_way = parametric_way self.pauli_list = list_gsuit_paulis self.shotN = shotN self.method = method self.parity_matrix = self.construct_parity_matrix( list_gsuit_paulis, n_qubits) self.verbose = verbose self.n_qubits = n_qubits self.cq = cq self.calibration = calibration if qc is not None: if qc.name[-4:] == 'yqvm' and self.cq is not None: raise NotImplementedError( 'manual qubit lattice feed' ' for PyQVM not yet implemented. please set cq=None') else: if self.method == 'QC': raise ValueError('method is QC but no QuantumComputer' ' object supplied.') # instantiate a new program and construct it for compilation prog = Program() if self.method == 'QC': ro = prog.declare('ro', memory_type='BIT', memory_size=self.n_qubits) if active_reset and self.method == 'QC': if not qc.name[-4:] == 'yqvm': # in case of PyQVM, can not contain reset statement prog += RESET() # circuit which produces reference state (f.ex. Hartree-Fock) prog += ref_state # produce which prepares an ansatz state starting # from a reference state (f.ex. UCCSD or swap network UCCSD) prog += ansatz self.term_keys = [] already_done = [] for pauli in list_gsuit_paulis: # save the id for each term self.term_keys.append(pauli.operations_as_set()) # also, we perform the necessary rotations # going from X or Y to Z basis for (i, st) in pauli.operations_as_set(): if (st == 'X' or st == 'Y') and i not in already_done: # note that 'i' is the *logical* index # corresponding to the pauli. if cq is not None: # if the logical qubit should be remapped # to physical qubits, access this cq prog += pauli_meas(cq[i], st) else: prog += pauli_meas(i, st) # if we already have rotated the basis # due to another term, don't do it again! already_done.append(i) if self.method != 'QC': self.pure_pyquil_program = Program(prog) else: # measure the qubits and assign the result # to classical register ro for i in range(self.n_qubits): if cq is not None: prog += MEASURE(cq[i], ro[i]) else: prog += MEASURE(i, ro[i]) prog2 = percolate_declares(prog) # wrap in shotN number of executions on the qc, # to get operator measurement by sampling prog2.wrap_in_numshots_loop(shots=self.shotN) # print(self.term_keys) # print circuits: # from pyquil.latex import to_latex # print(to_latex(prog2)) # input() if qc.name[-4:] == 'yqvm': self.pyqvm_program = prog2 else: self.pyquil_executable = qc.compile(prog2) # print("compiled") # print(to_latex(qc.compiler.quil_to_native_quil(prog2))) # input() # now about calibration if self.calibration and self.method != 'QC': # turn off calibration if not QC. self.calibration = False if self.calibration: # prepare and run the calibration experiments prog = Program() ro = prog.declare('ro', memory_type='BIT', memory_size=self.n_qubits) if active_reset: if not qc.name[-4:] == 'yqvm': # in case of PyQVM, can not contain reset statement prog += RESET() # circuit which produces reference state, # which is in the case of calibration experiments # the same as the out_operators. already_done = [] for pauli in list_gsuit_paulis: # also, we perform the necessary rotations # going to X/Y/Z =1 state for (i, st) in pauli.operations_as_set(): if (st == 'X' or st == 'Y') and i not in already_done: # note that 'i' is the *logical* index # corresponding to the pauli. if cq is not None: # if the logical qubit should be remapped # to physical qubits, access this cq prog += pauli_meas(cq[i], st).dagger() else: prog += pauli_meas(i, st).dagger() # if we already have rotated the basis # due to another term, don't do it again! already_done.append(i) # measurement now already_done = [] for pauli in list_gsuit_paulis: # also, we perform the necessary rotations # going from X or Y to Z basis for (i, st) in pauli.operations_as_set(): if (st == 'X' or st == 'Y') and i not in already_done: # note that 'i' is the *logical* index # corresponding to the pauli. if cq is not None: # if the logical qubit should be remapped # to physical qubits, access this cq prog += pauli_meas(cq[i], st) else: prog += pauli_meas(i, st) # if we already have rotated the basis # due to another term, don't do it again! already_done.append(i) # measure the qubits and assign the result # to classical register ro for i in range(self.n_qubits): if cq is not None: prog += MEASURE(cq[i], ro[i]) else: prog += MEASURE(i, ro[i]) prog2 = percolate_declares(prog) # wrap in shotN number of executions on the qc, # to get operator measurement by sampling prog2.wrap_in_numshots_loop(shots=self.shotN) if qc.name[-4:] == 'yqvm': self.pyqvm_program = prog2 bitstrings = qc.run(self.pyqvm_program) # compile to native quil if it's not a PYQVM else: pyquil_executable = qc.compile(prog2) bitstrings = qc.run(pyquil_executable) # start data processing # this matrix computes the pauli string parity, # and stores that for each bitstring is_odd = np.mod(bitstrings.dot(self.parity_matrix), 2) # if the parity is odd, the bitstring gives a -1 eigenvalue, # and +1 vice versa. # sum over all bitstrings, average over shotN shots, # and weigh each pauli string by its coefficient e_array = 1 - 2 * np.sum(is_odd, axis=0) / self.shotN self.calibration_norms = e_array print(f"calibration_norms: {e_array}")
def get_n_bit_adder_results(qc: QuantumComputer, n_bits: int, registers: Optional[Tuple[Sequence[int], Sequence[int], int, int]] = None, qubits: Optional[Sequence[int]] = None, in_x_basis: bool = False, num_shots: int = 100, use_param_program: bool = False, use_active_reset: bool = True, show_progress_bar: bool = False) \ -> Sequence[Sequence[Sequence[int]]]: """ Convenient wrapper for collecting the results of addition for every possible pair of n_bits long summands. :param qc: the quantum resource on which to run each addition :param n_bits: the number of bits of one of the summands (each summand is the same length) :param registers: optional explicit qubit layout of each register passed to :func:`adder` :param qubits: available subset of qubits of the qc on which to run the circuits. :param in_x_basis: if true, prepare the bitstring-representation of the numbers in the x basis and subsequently performs all addition logic in the x basis. :param num_shots: the number of times to sample the output of each addition :param use_param_program: whether or not to use a parameterized program for state preparation. Doing so should speed up overall execution on a QPU. :param use_active_reset: whether or not to use active reset. Doing so will speed up execution on a QPU. :param show_progress_bar: displays a progress bar via tqdm if true. :return: A list of n_shots many outputs for each possible summation of two n_bit long summands, listed in increasing numerical order where the label is the 2n bit number represented by num = a_bits | b_bits for the addition of a + b. """ if registers is None: registers = get_qubit_registers_for_adder(qc, n_bits, qubits) reset_prog = Program() if use_active_reset: reset_prog += RESET() add_prog = Program() if use_param_program: dummy_num = [0 for _ in range(n_bits)] add_prog = adder(dummy_num, dummy_num, *registers, in_x_basis=in_x_basis, use_param_program=True) all_results = [] # loop over all binary strings of length n_bits for bits in tqdm(all_bitstrings(2 * n_bits), disable=not show_progress_bar): # split the binary number into two numbers # which are the binary numbers the user wants to add. # They are written from (MSB .... LSB) = (a_n, ..., a_1, a_0) num_a = bits[:n_bits] num_b = bits[n_bits:] if not use_param_program: add_prog = adder(num_a, num_b, *registers, in_x_basis=in_x_basis, use_param_program=False) prog = reset_prog + add_prog prog.wrap_in_numshots_loop(num_shots) nat_quil = qc.compiler.quil_to_native_quil(prog) exe = qc.compiler.native_quil_to_executable(nat_quil) # Run it on the QPU or QVM if use_param_program: results = qc.run(exe, memory_map={REG_NAME: bits}) else: results = qc.run(exe) all_results.append(results) return all_results