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