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)
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)