Beispiel #1
0
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
Beispiel #5
0
    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
Beispiel #6
0
    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, :]
Beispiel #7
0
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