def lift_bitstring_function(n, f): """Lifts a function defined on single bitstrings to arrays of bitstrings. Args: n: The number of bits in the bitsring. f: The bitstring function, which produces a float value. Returns: A function which, given a K x n array of 0/1 values, returns the mean of f applied across the K rows. """ bss = all_bitstrings(n).astype(np.float64) vals = np.apply_along_axis(f, 1, bss) # normalize to be between 0 and 1 m, M = np.min(vals), np.max(vals) if np.isclose(m, M): vals[:] = 0.5 else: vals -= m vals *= 1 / (M - m) def _fn(bitstrings): indices = np.apply_along_axis(bitstring_index, 1, bitstrings) return np.mean(vals[indices]) return _fn
def get_error_hamming_distributions_from_results(results: Sequence[Sequence[Sequence[int]]]) \ -> Sequence[Sequence[float]]: """ Get the distribution of the hamming weight of the error vector (number of bits flipped between output and expected answer) for each possible pair of two n_bit summands using results output by get_n_bit_adder_results :param results: a list of results output from a call to get_n_bit_adder_results :return: the relative frequency of observing each hamming weight, 0 to n_bits+1, for the error that occurred when adding each pair of two n_bit summands """ num_shots = len(results[0]) n_bits = len(results[0][0]) - 1 hamming_wt_distrs = [] # loop over all binary strings of length n_bits for result, bits in zip(results, all_bitstrings(2 * n_bits)): # Input nums are written from (MSB .... LSB) = (a_n, ..., a_1, a_0) num_a = bit_array_to_int(bits[:n_bits]) num_b = bit_array_to_int(bits[n_bits:]) # add the numbers ans = num_a + num_b ans_bits = int_to_bit_array(ans, n_bits + 1) # record the fraction of shots that resulted in an error of the given weight hamming_wt_distr = [0. for _ in range(len(ans_bits) + 1)] for shot in result: # multiply relative hamming distance by the length of the output for the weight wt = len(ans_bits) * hamming(ans_bits, shot) hamming_wt_distr[int(wt)] += 1. / num_shots hamming_wt_distrs.append(hamming_wt_distr) return hamming_wt_distrs
def sample_bitstrings(self, n_samples, tol_factor: float = 1e8): """ Sample bitstrings from the distribution defined by the wavefunction. Qubit 0 is at ``out[:, 0]``. :param n_samples: The number of bitstrings to sample :param tol_factor: Tolerance to set imaginary probabilities to zero, relative to machine epsilon. :return: An array of shape (n_samples, n_qubits) """ if self.rs is None: raise ValueError( "You have tried to perform a stochastic operation without setting the " "random state of the simulator. Might I suggest using a PyQVM object?" ) # for np.real_if_close the actual tolerance is (machine_eps * tol_factor), # where `machine_epsilon = np.finfo(float).eps`. If we use tol_factor = 1e8, then the # overall tolerance is \approx 2.2e-8. probabilities = np.real_if_close(np.diagonal(self.density), tol=tol_factor) # Next set negative probabilities to zero probabilities = [0 if p < 0.0 else p for p in probabilities] # Ensure they sum to one probabilities = probabilities / np.sum(probabilities) possible_bitstrings = all_bitstrings(self.n_qubits) inds = self.rs.choice(2**self.n_qubits, n_samples, p=probabilities) bitstrings = possible_bitstrings[inds, :] bitstrings = np.flip(bitstrings, axis=1) # qubit ordering: 0 on the left. return bitstrings
def get_success_probabilities_from_results(results: Sequence[Sequence[Sequence[int]]]) \ -> Sequence[float]: """ Get the probability of a successful addition for each possible pair of two n_bit summands from the results output by get_n_bit_adder_results :param results: a list of results output from a call to get_n_bit_adder_results :return: the success probability for the summation of each possible pair of n_bit summands """ num_shots = len(results[0]) n_bits = len(results[0][0]) - 1 probabilities = [] # loop over all binary strings of length n_bits for result, bits in zip(results, all_bitstrings(2 * n_bits)): # Input nums are written from (MSB .... LSB) = (a_n, ..., a_1, a_0) num_a = bit_array_to_int(bits[:n_bits]) num_b = bit_array_to_int(bits[n_bits:]) # add the numbers ans = num_a + num_b ans_bits = int_to_bit_array(ans, n_bits + 1) # a success occurs if a shot matches the expected ans bit for bit probability = 0 for shot in result: if np.array_equal(ans_bits, shot): probability += 1. / num_shots probabilities.append(probability) return probabilities
def sample_bitstrings(self, n_samples): """ Sample bitstrings from the distribution defined by the wavefunction. Qubit 0 is at ``out[:, 0]``. :param n_samples: The number of bitstrings to sample :return: An array of shape (n_samples, n_qubits) """ if self.rs is None: raise ValueError("You have tried to perform a stochastic operation without setting the " "random state of the simulator. Might I suggest using a PyQVM object?") probabilities = np.abs(self.wf) ** 2 possible_bitstrings = all_bitstrings(self.n_qubits) inds = self.rs.choice(2 ** self.n_qubits, n_samples, p=probabilities) bitstrings = possible_bitstrings[inds, :] bitstrings = np.flip(bitstrings, axis=1) # qubit ordering: 0 on the left. return bitstrings
def sample_bitstrings(self, n_samples): """ Sample bitstrings from the distribution defined by the wavefunction. Qubit 0 is at ``out[:, 0]``. :param n_samples: The number of bitstrings to sample :return: An array of shape (n_samples, n_qubits) """ if self.rs is None: raise ValueError( "You have tried to perform a stochastic operation without setting the " "random state of the simulator. Might I suggest using a PyQVM object?" ) # note on reshape: it puts bitstrings in lexicographical order. # would you look at that .. _all_bitstrings returns things in lexicographical order! # reminder: qubit 0 is on the left in einsum simulator. probabilities = np.abs(self.wf.reshape(-1))**2 possible_bitstrings = all_bitstrings(self.n_qubits) inds = self.rs.choice(2**self.n_qubits, n_samples, p=probabilities) return possible_bitstrings[inds, :]
def robust_phase_estimate( experiment: DataFrame, results_label="Results") -> Union[float, Sequence[float]]: """ Provides the estimate of the phase for an RPE experiment with results. In the 1q case this is simply a convenient wrapper around get_moments() and estimate_phase_from_moments() which do all of the analysis; see those methods above for details. For multiple qubits this method determines which possible outputs are consistent with the post-selection-state and the possible non-z-basis measurement qubit. For each choice of the latter, all such possible outcomes correspond to measurement of a different relative phase. get_moments() is called on a dataframe with rows consistent with the particular non-z-basis measurement qubit and each outcome. If there is no post-select state then the number of relative phases estimated is equal to the dimension of the Hilbert space. :param experiment: an RPE experiment with results. :param results_label: label for the column with results from which the moments are estimated :return: an estimate of the phase of the rotation program passed into generate_rpe_experiments If the rotation program is multi-qubit then there will be 2**(len(meas_qubits) - len(post_select_state) - 1) different relative phases estimated and returned. """ meas_qubits = experiment["Measure Qubits"].values[0] if len(meas_qubits) == 1: moments = get_moments(experiment, results_label=results_label) phase = estimate_phase_from_moments(*moments) return phase else: state = [None] * len(meas_qubits) if "Post Select State" in experiment.columns.values: state = experiment["Post Select State"].values[0] relative_phases = [] for idx, meas_q in enumerate(meas_qubits): if state[idx] is not None: # qubit is never measured in X/Y basis and is only used for post-selection continue # idx is 0 # get only the rows where the meas_q is actually the qubit being measured in X/Y basis expt = experiment[experiment["Non-Z-Basis Meas Qubit"] == meas_q] # Each distinct outcome on {qubits - meas_q} corresponds to the estimation of the # relative phase between different pairs of eigenvectors. Here we iterate over each # unique outcome, discard outcomes that don't match the post-selected state, # and estimate the phase corresponding to this outcome. for outcome in all_bitstrings(len(meas_qubits) - 1): full = np.insert( outcome, idx, 0) # fill in the meas_q for comparison to state matches = [ bit == full[j] for j, bit in enumerate(state) if bit is not None ] if not all(matches): # the outcome violates a post-selection continue moments = get_moments(expt, outcome, results_label) relative_phases.append(estimate_phase_from_moments(*moments)) return relative_phases
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