Beispiel #1
0
 def test_binary_decimal_to_float_conversion(self):
     """Tests converting binary decimals (e.g., 0.10 = 0.5 or 0.01 = 0.25) to floats, and vice versa."""
     for num in np.linspace(0, 0.99, 25):
         self.assertAlmostEqual(
             QSVE.binary_decimal_to_float(QSVE.to_binary_decimal(num,
                                                                 nbits=30),
                                          big_endian=True), num)
Beispiel #2
0
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)