Exemple #1
0
    def test_singular_values_from_theta_values_for_two_by_two_identity_matrix(
            self):
        """Tests the correct theta values (in the range [0, 1] are measured for the identity matrix.

        These theta values are 0.25 and 0.75, or 0.01 and 0.11 as binary decimals, respectively.

        The identity matrix A = [[1, 0], [0, 1]] has singular value 1 and Froebenius norm sqrt(2). It follows that

                                    sigma / ||A||_F = 1 / sqrt(2)

        Since                       cos(pi * theta) = sigma / ||A||_F,

        we must have                cos(pi * theta) = 1 / sqrt(2),

        which means that theta = - 0.25 or theta = 0.25. After mapping from the interval [-1/2, 1/2] to the interval
        [0, 1] via

                                    theta ----> theta           (if 0 <= theta <= 1 / 2)
                                    theta ----> theta + 1       (if -1 / 2 <= theta < 0)

        (which is what we measure in QSVE), the possible outcomes are thus 0.25 and 0.75. These correspond to binary
        decimals 0.01 and 0.10, respectively.

        This test does QSVE on the identity matrix using 2, 3, 4, 5, and 6 precision qubits for QPE.
        """
        # Define the identity matrix
        matrix = np.identity(2)

        # Create the QSVE instance
        qsve = QSVE(matrix)

        for nprecision_bits in [2, 3, 4, 5, 6]:
            # Get the circuit to perform QSVE with terminal measurements on the QPE register
            circuit = qsve.create_circuit(nprecision_bits=nprecision_bits,
                                          terminal_measurements=True)

            # Run the quantum circuit for QSVE
            sim = BasicAer.get_backend("qasm_simulator")
            job = execute(circuit, sim, shots=10000)

            # Get the output bit strings from QSVE
            res = job.result()
            counts = res.get_counts()
            thetas_binary = np.array(list(counts.keys()))

            # Make sure there are only two measured values
            self.assertEqual(len(thetas_binary), 2)

            # Convert the measured angles to floating point values
            thetas = [
                qsve.binary_decimal_to_float(binary_decimal, big_endian=False)
                for binary_decimal in thetas_binary
            ]
            thetas = [qsve.convert_measured(theta) for theta in thetas]

            # Make sure the theta values are correct
            self.assertEqual(len(thetas), 2)
            self.assertIn(0.25, thetas)
            self.assertIn(-0.25, thetas)
Exemple #2
0
    def test_singular_values_two_by_two_three_pi_over_eight_singular_vector2(
            self):
        """Tests computing the singular values of the matrix

                    A = [[cos(3 * pi / 8), 0],
                         [0, sin(3 * pi / 8)]]

        with an input singular vector. Checks that only one singular value is present in the measurement outcome.
        """
        # Define the matrix
        matrix = np.array([[np.cos(3 * np.pi / 8), 0],
                           [0, np.sin(3 * np.pi / 8)]])

        # Do the classical SVD. (Note: We could just access the singular values from the diagonal matrix elements.)
        _, sigmas, _ = np.linalg.svd(matrix)

        # Get the quantum circuit for QSVE
        for nprecision_bits in range(3, 7):
            qsve = QSVE(matrix)
            circuit = qsve.create_circuit(nprecision_bits=nprecision_bits,
                                          init_state_row_and_col=[0, 1, 0, 0],
                                          terminal_measurements=True)

            # Run the quantum circuit for QSVE
            sim = BasicAer.get_backend("qasm_simulator")
            job = execute(circuit, sim, shots=10000)

            # Get the output bit strings from QSVE
            res = job.result()
            counts = res.get_counts()
            thetas_binary = np.array(list(counts.keys()))

            # Convert from the binary strings to theta values
            computed = [
                qsve.convert_measured(qsve.binary_decimal_to_float(bits))
                for bits in thetas_binary
            ]

            # Convert from theta values to singular values
            qsigmas = [
                np.cos(np.pi * theta) for theta in computed if theta > 0
            ]

            # Sort the sigma values for comparison
            sigmas = list(sorted(sigmas))
            qsigmas = list(sorted(qsigmas))

            # Make sure the quantum solution is close to the classical solution
            self.assertTrue(np.allclose(sigmas[1], qsigmas))
class QuantumRecommendation:
    """Class for a quantum recommendation system."""
    def __init__(self, preference_matrix, nprecision_bits=3):
        """Initializes a QuantumRecommendation.

        Args:
            preference_matrix

            nprecision_bits : int
                Number of precision qubits to use in QPE.
        """
        self._matrix = deepcopy(preference_matrix)
        self._precision = nprecision_bits
        self._qsve = QSVE(preference_matrix)

    @property
    def matrix(self):
        return self._matrix

    @property
    def num_users(self):
        return len(self._matrix)

    @property
    def num_products(self):
        return np.shape(self._matrix)[1]

    def _threshold(self,
                   circuit,
                   register,
                   ctrl_string,
                   measure_flag_qubit=True):
        """Adds gates to the input circuit to perform a thresholding operation, which discards singular values
        below some threshold, or minimum value.

        Args:
            circuit : qiskit.QuantumCircuit
                Circuit to the thresholding operation (circuit) to.

            register : qiskit.QuantumRegister
                A register (in the input circuit) to threshold on. For recommendation systems, this will always be
                the "singular value register."

            minimum_value : float
                A floating point value in the range [0, 1]. All singular values above minimum_value will be kept for
                the recommendation, and values below will be discarded.

        The thresholding circuit adds a register of three ancilla qubits to the circuit and has the following structure:

                            |0> --------O--------@-----------------------@--------O--------
                                        |        |                       |        |
                            |0> --------O--------@-----------------------@--------O--------
                                        |        |                       |        |
             register       |0> --------O--------@-----------------------@--------O--------
                                        |        |                       |        |
                            |0> --------|--------|-----------------------|--------|--------
                                        |        |                       |        |
                            |0> --------|--------|-----------------------|--------|--------
                                        |        |                       |        |
                                        |        |                       |        |
                            |0> -------[X]-------|---------@------------[X]-------|--------
                                                 |         |                      |
        threshold register  |0> ----------------[X]--------|-----@---------------[X]-------
                                                           |     |
                            |0> --------------------------[X]---[X]------------------------

        The final qubit in the threshold_register is a "flag qubit" which determines whether or not to accept the
        recommendation. That is, recommendation results are post-selected on this qubit.

        Returns:
            None

        Modifies:
            The input circuit.
        """
        # Determine the number of controls
        ncontrols = (ctrl_string + "1").index("1")

        # Edge case in which no operations are added
        if ncontrols == 0:
            return

        # Make sure there is at least one control
        if ncontrols == len(ctrl_string):
            raise ValueError(
                "Argument ctrl_string should have at least one '1'.")

        # Add the ancilla register of three qubits to the circuit
        ancilla = QuantumRegister(3, name="threshold")
        circuit.add_register(ancilla)

        # ==============================
        # Add the gates for thresholding
        # ==============================

        # Do the first "anti-controlled" Tofolli
        for ii in range(ncontrols):
            circuit.x(register[ii])
        mct(circuit, register[:ncontrols], ancilla[0], None, mode="noancilla")
        for ii in range(ncontrols):
            circuit.x(register[ii])

        # Do the second Tofolli
        mct(circuit, register[:ncontrols], ancilla[1], None, mode="noancilla")

        # Do the copy CNOTs in the ancilla register
        circuit.cx(ancilla[0], ancilla[2])
        circuit.cx(ancilla[1], ancilla[2])

        # Uncompute the second Tofolli
        mct(circuit, register[:ncontrols], ancilla[1], None, mode="noancilla")

        # Uncompute the first "anti-controlled" Tofolli
        for ii in range(ncontrols):
            circuit.x(register[ii])
        mct(circuit, register[:ncontrols], ancilla[0], None, mode="noancilla")
        for ii in range(ncontrols):
            circuit.x(register[ii])

        # Add the "flag qubit" measurement, if desired
        if measure_flag_qubit:
            creg = ClassicalRegister(1, name="flag")
            circuit.add_register(creg)
            circuit.measure(ancilla[2], creg[0])

    def create_circuit(self,
                       user,
                       threshold,
                       measurements=True,
                       return_registers=False,
                       logical_barriers=False,
                       swaps=True):
        """Returns the quantum circuit to recommend product(s) to a user.

        Args:
            user : numpy.ndarray
                Mathematically, a vector whose length is equal to the number of columns in the preference matrix.
                In the context of recommendation systems, this is a vector of "ratings" for "products,"
                where each vector elements corresponds to a rating for product 0, 1, ..., N. Examples:
                    user = numpy.array([1, 0, 0, 0])
                        The user likes the first product, and we have no information about the other products.

                    user = numpy.array([0.5, 0.9, 0, 0])
                        The user is neutral about the first product, and rated the second product highly.

            threshold : float in the interval [0, 1)
                Only singular vectors with singular values greater than `threshold` are kept for making recommendations.
                Note: Singular values are assumed to be normalized (via sigma / ||P||_F) to lie in the interval [0, 1).

                This is related to the low rank approximation in the preference matrix P. The larger the threshold,
                the lower the assumed rank of P. Examples:
                    threshold = 0.
                        All singular values are kept. This means we have "complete knowledge" about the user, and
                        recommendations are made solely based off their preferences. (No quantum circuit is needed.)

                    threshold = 0.5
                        All singular values greater than 0.5 are kept for recommendations.

                    threshold = 1 (invalid)
                        No singular values are kept for recommendations. This throws an error.

            measurements : bool
                Determines whether the product register is measured at the end of the circuit.

            return_registers : bool
                If True, registers in the circuit are returned in the order:
                    (1) Circuit.
                    (2) QPE register.
                    (3) User register.
                    (4) Product register.
                    (5) Classical register for product measurements (if measurements == True).

                Note: Registers can always be accessed through the return circuit. This is provided for convenience.

            logical_barriers : bool
                Determines whether to place barriers in the circuit separating subroutines.

            swaps : bool
                If True, the product register qubits are swapped to put the measurement outcome in big endian.

        Returns : qiskit.QuantumCircuit (and qiskit.QuantumRegisters, if desired)
            A quantum circuit implementing the quantum recommendation systems algorithm.
        """
        # Make sure the user is valid
        self._validate_user(user)

        # Convert the threshold value to the control string
        ctrl_string = self._threshold_to_control_string(threshold)

        # Create the QSVE circuit
        circuit, qpe_register, user_register, product_register = self._qsve.create_circuit(
            nprecision_bits=self._precision,
            logical_barriers=logical_barriers,
            load_row_norms=True,
            init_state_col=user,
            return_registers=True,
            row_name="user",
            col_name="product")

        # Add the thresholding operation on the singular value (QPE) register
        self._threshold(circuit,
                        qpe_register,
                        ctrl_string,
                        measure_flag_qubit=True)

        # Add the inverse QSVE circuit
        circuit += self._qsve.create_circuit(nprecision_bits=self._precision,
                                             logical_barriers=logical_barriers,
                                             load_row_norms=False,
                                             init_state_col=None,
                                             return_registers=False,
                                             row_name="user",
                                             col_name="product").inverse()

        # Swap the qubits in the product register to put resulting bit string in big endian
        if swaps:
            for ii in range(len(product_register) // 2):
                circuit.swap(product_register[ii], product_register[-ii - 1])

        # Add measurements to the product register, if desired
        if measurements:
            creg = ClassicalRegister(len(product_register),
                                     name="recommendation")
            circuit.add_register(creg)
            circuit.measure(product_register, creg)

        if return_registers:
            if measurements:
                return circuit, qpe_register, user_register, product_register, creg
            return circuit, qpe_register, user_register, product_register
        return circuit

    def run_and_return_counts(self, user, threshold, shots=10000):
        """Runs the quantum circuit recommending products for the given user and returns the raw counts."""
        circuit = self.create_circuit(user,
                                      threshold,
                                      measurements=True,
                                      logical_barriers=False)

        job = execute(circuit,
                      BasicAer.get_backend("qasm_simulator"),
                      shots=shots)

        results = job.result()

        return results.get_counts()

    def recommend(self,
                  user,
                  threshold,
                  shots=10000,
                  with_probabilities=True,
                  products_as_ints=True):
        """Returns a recommendation for a specified user."""
        # Run the quantum recommendation and get the counts
        counts = self.run_and_return_counts(user, threshold, shots)

        # Remove all outcomes with flag qubit measured as zero (assuming the flag qubit is measured)
        post_selected = []
        for bits, count in counts.items():
            # This checks if two different registers have been measured (i.e., checks if the flag qubit has been
            # measured or not).
            if " " in bits:
                product, flag = bits.split()
                if flag == "1":
                    post_selected.append((product, count))
            else:
                post_selected.append((bits, count))

        # Get the number of post-selected measurements for normalization
        new_shots = sum([x[1] for x in post_selected])

        # Convert the bit string outcomes to ints
        products = []
        probs = []
        for (product, count) in post_selected:
            if products_as_ints:
                product = self._binary_string_to_int(product, big_endian=True)
            products.append(product)

            if with_probabilities:
                probs.append(count / new_shots)
        if with_probabilities:
            return products, probs
        return products

    def classical_recommendation(self, user, rank, quantum_format=True):
        """Returns a recommendation for a specified user via classical singular value decomposition.

        Args:
            user : numpy.ndarray
                A vector whose length is equal to the number of columns in the preference matrix.
                See help(QuantumRecommendation.create_circuit) for more context.

            rank : int in the range (0, minimum dimension of preference matrix)
                Specifies how many singular values (ordered in decreasing order) to keep in the recommendation.

            quantum_format : bool
                Specifies format of returned value(s).

                If False, a vector of product recommendations is returned.
                    For example, vector = [1, 0, 0, 1] means the first and last products are recommended.

                If True, two arguments are returned in the order:
                    (1) list<int>
                        List of integers specifying products to recommend.
                    (2) list<float>
                        List of floats specifying the probabilities to recommend products.

                    For example:
                        products = [0, 2], probabilities = [0.36, 0.64] means
                        product 0 is recommended with probability 0.36, product 2 is recommend with probability 0.64.

        Returns : Union[numpy.ndarray, tuple(list<int>, list<float>)
            See `quantum_format` above for more information.
        """
        # Make sure the user and rank are ok
        self._validate_user(user)
        self._validate_rank(rank)

        # Do the classical SVD
        _, _, vmat = np.linalg.svd(self.matrix, full_matrices=True)

        # Do the projection
        recommendation = np.zeros_like(user, dtype=np.float64)
        for ii in range(rank):
            recommendation += np.dot(np.conj(vmat[ii]), user) * vmat[ii]

        if np.allclose(recommendation, np.zeros_like(recommendation)):
            raise RankError(
                "Given rank is smaller than the rank of the preference matrix. Recommendations "
                "cannot be made for all users.")

        # Return the squared values for probabilities
        probabilities = (recommendation /
                         np.linalg.norm(recommendation, ord=2))**2

        # Return the vector if quantum_format is False
        if not quantum_format:
            return probabilities

        # Format the same as the quantum recommendation
        prods = []
        probs = []
        for (ii, p) in enumerate(probabilities):
            if p > 0:
                prods.append(ii)
                probs.append(p)
        return prods, probs

    def _validate_user(self, user):
        """Validates a user (vector).

        If an invalid user for the recommendation system is given, a UserError is thrown. Else, nothing happens.

        Args:
            user : numpy.ndarray
                A vector representing a user in a recommendation system.
        """
        # Make sure the user is of the correct type
        if not isinstance(user, (list, tuple, np.ndarray)):
            raise UserVectorError(
                "Invalid type for user. Accepted types are list, tuple, and numpy.ndarray."
            )

        # Make sure the user vector has the correct length
        if len(user) != self.num_products:
            raise UserVectorError(
                f"User vector should have length {self.num_products} but has length {len(user)}"
            )

        # Make sure at least one element of the user vector is nonzero
        all_zeros = np.zeros_like(user)
        if np.allclose(user, all_zeros):
            raise UserVectorError(
                "User vector is all zero and thus contains no information. "
                "At least one element of the user vector must be nonzero.")

    def _validate_rank(self, rank):
        """Throws a RankError if the rank is not valid, else nothing happens."""
        if rank <= 0 or rank > self.num_users:
            raise RankError(
                "Rank must be in the range 0 < rank <= number of users.")

    @staticmethod
    def _to_binary_decimal(decimal, nbits=5):
        """Converts a decimal in base ten to a binary decimal string.

        Args:
            decimal : float
                Floating point value in the interval [0, 1).

            nbits : int
                Number of bits to use in the binary decimal.

        Return type:
            str
        """
        if 1 <= decimal < 0:
            raise ValueError(
                "Argument decimal should satisfy 0 <= decimal < 1.")

        binary = ""
        for ii in range(1, nbits + 1):
            if decimal * 2**ii >= 1:
                binary += "1"
                decimal = (decimal * 10) % 1
            else:
                binary += "0"
        return binary

    @staticmethod
    def _binary_string_to_int(bitstring, big_endian=True):
        """Returns the integer equivalent of the binary string.

        Args:
            bitstring : str
                String of characters "1" and "0".

            big_endian : bool
                Dictates whether string is big endian (most significant bit first) or little endian.
        """
        if not big_endian:
            bitstring = str(reversed(bitstring))
        val = 0
        nbits = len(bitstring)
        for (n, bit) in enumerate(bitstring):
            if bit == "1":
                val += 2**(nbits - n - 1)
        return val

    @staticmethod
    def _rank_to_ctrl_string(rank, length):
        """Converts the rank to a ctrl_string for thresholding.

        Args:
            rank : int
                Assumed rank of a system (cutoff for singular values).

            length : int
                Number of characters for the ctrl_string.

        Returns : str
            Binary string (base 2) representation of the rank with the given length.
        """
        pass

    def _threshold_to_control_string(self, threshold):
        """Returns a control string for the threshold circuit which keeps all values strictly above the threshold."""
        # Make sure the threshold is ok
        if threshold < 0 or threshold > 1:
            raise ThresholdError(
                "Argument threshold must satisfy 0 <= threshold <= 1.")

        # Compute the angle 0 <= theta <= 1 for this threshold singular value
        theta = 1 / np.pi * np.arccos(threshold)

        # Return the binary decimal of theta
        return self._qsve.to_binary_decimal(theta, nbits=self._precision)

    def __str__(self):
        return "Quantum Recommendation System with {} users and {} products.".format(
            self.num_users, self.num_products)
Exemple #4
0
class LinearSystemSolverQSVE:
    """Quantum algorithm for solving linear systems of equations based on quantum singular value estimation (QSVE)."""
    def __init__(self, Amatrix, bvector, precision=3, cval=0.5):
        """Initializes a LinearSystemSolver.

        Args:
            Amatrix : numpy.ndarray
                Matrix in the linear system Ax = b.

            bvector : numpy.ndarray
                Vector in the linear system Ax = b.

            precision : int
                Number of bits of precision to use in the QSVE subroutine.
        """
        self._matrix = deepcopy(Amatrix)
        self._vector = deepcopy(bvector)
        self._precision = precision
        self._cval = cval
        self._qsve = QSVE(Amatrix,
                          singular_vector=bvector,
                          nprecision_bits=precision)

    @property
    def matrix(self):
        return self._matrix

    @property
    def vector(self):
        return self._vector

    def classical_solution(self, normalized=True):
        """Returns the solution of the linear system found classically.

        Args:
            normalized : bool (default value = True)
                If True, the classical solution is normalized (by the L2 norm), else it is un-normalized.
        """
        xclassical = np.linalg.solve(self._matrix, self._vector)
        if normalized:
            return xclassical / np.linalg.norm(xclassical, ord=2)
        return xclassical

    def _hhl_rotation(self,
                      circuit,
                      eval_register,
                      ancilla_qubit,
                      constant=0.5):
        """Adds the gates for the HHL rotation to perform the transformation


            sum_j beta_j |lambda_j>  ---->  sum_j beta_j / lambda_j |lambda_j>

        Args:

        """
        # The number of controls is the number of qubits in the eval_register
        ncontrols = len(eval_register)

        for ii in range(2**ncontrols):
            # Get the bitstring for this index
            bitstring = np.binary_repr(ii, ncontrols)

            # Do the initial sequence of NOT gates to get controls/anti-controls correct
            for (ind, bit) in enumerate(bitstring):
                if bit == "0":
                    circuit.x(eval_register[ind])

            # Determine the theta value in this amplitude
            theta = self._qsve.binary_decimal_to_float(bitstring)

            # Compute the eigenvalue in this register
            eigenvalue = self._qsve.matrix_norm() * np.cos(np.pi * theta)

            # TODO: Is this correct to do?
            if np.isclose(eigenvalue, 0.0):
                continue

            # Determine the angle of rotation for the Y-rotation
            angle = 2 * np.arccos(constant / eigenvalue)

            # Do the controlled Y-rotation
            mcry(circuit,
                 angle,
                 eval_register,
                 ancilla_qubit,
                 None,
                 mode="noancilla")

            # Do the final sequence of NOT gates to get controls/anti-controls correct
            for (ind, bit) in enumerate(bitstring):
                if bit == "0":
                    circuit.x(eval_register[ind])

    def create_circuit(self, return_registers=False):
        """Creates the circuit that solves the linear system Ax = b."""
        # Get the QSVE circuit
        circuit, qpe_register, row_register, col_register = self._qsve.create_circuit(
            return_registers=True)

        # Make a copy to take the inverse of later
        qsve_circuit = deepcopy(circuit)

        # Add the ancilla register (of one qubit) for the HHL rotation
        ancilla = QuantumRegister(1, name="anc")
        circuit.add_register(ancilla)

        # Do the HHL rotation
        self._hhl_rotation(circuit, qpe_register, ancilla[0], self._cval)

        # Add the inverse QSVE circuit (without the initial data loading subroutines)
        circuit += self._qsve.create_circuit(initial_loads=False).inverse()

        if return_registers:
            return circuit, qpe_register, row_register, col_register, ancilla
        return circuit

    def _run(self, simulator, shots):
        """Runs the quantum circuit and returns the measurement counts."""
        pass

    def quantum_solution(self):
        pass

    def compute_expectation(self, observable):
        pass