def test_isometries_large_matrix(self): """Tests that the row isometry is indeed an isometry for random 16 x 16 matrices.""" iden = np.identity(16) for _ in range(100): # Get a random Hermitian matrix matrix = np.random.randn(16, 16) matrix += matrix.conj().T # Get the row isometry qsve = QSVE(matrix) umat = qsve.row_isometry() vmat = qsve.norm_isometry() # Make sure U^dagger U = I self.assertTrue(np.allclose(umat.conj().T @ umat, iden)) self.assertTrue(np.allclose(vmat.conj().T @ vmat, iden)) # Make sure U^dagger V = V^dagger U = A / ||A||_F self.assertTrue( np.allclose(umat.conj().T @ vmat, matrix / np.linalg.norm(matrix, ord="fro"))) self.assertTrue( np.allclose(vmat.conj().T @ umat, matrix / np.linalg.norm(matrix, ord="fro"))) # Make sure rank(U U^dagger) = 256 self.assertEqual(np.linalg.matrix_rank(vmat @ vmat.conj().T), len(matrix))
def test_isometries_four_by_four(self): """Tests that the row (norm) isometry is indeed an isometry for random four by four matrices.""" iden = np.identity(4) for _ in range(100): # Get a Hermitian matrix matrix = np.random.randn(4, 4) matrix += matrix.conj().T # Get a QSVE object and compute the row isometry qsve = QSVE(matrix) umat = qsve.row_isometry() vmat = qsve.norm_isometry() # Make sure U^dagger U = I = V^dagger V self.assertTrue(np.allclose(umat.conj().T @ umat, iden)) self.assertTrue(np.allclose(vmat.conj().T @ vmat, iden)) # Make sure U^dagger V = V^dagger U = A / ||A||_F self.assertTrue( np.allclose(umat.conj().T @ vmat, matrix / np.linalg.norm(matrix, ord="fro"))) self.assertTrue( np.allclose(vmat.conj().T @ umat, matrix / np.linalg.norm(matrix, ord="fro"))) # Make sure rank(U U^dagger) = 4 self.assertEqual(np.linalg.matrix_rank(umat @ umat.conj().T), len(matrix))
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)
def test_unitary_evals_matrix_singular_values_identity_2by2(self): """Tests that the eigenvalues of the unitary are the matrix singular values.""" # Dimension of system dim = 2 # Define the matrix and QSVE object matrix = np.identity(dim) qsve = QSVE(matrix) # Get the (normalized) singular values of the matrix sigmas = qsve.singular_values_classical(normalized=True) # Get the eigenvalues of the QSVE unitary evals, _ = np.linalg.eig(qsve.unitary()) # Make sure there are the correct number of eigenvalues self.assertEqual(len(evals), dim**2) qsigmas = [] for eval in evals: qsigma = qsve.unitary_eval_to_singular_value(eval) if qsigma not in qsigmas: qsigmas.append(qsigma) self.assertTrue(np.allclose(qsigmas, sigmas))
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_non_square(self): """Tests QSVE on a simple non-square matrix.""" matrix = np.array([[1, 0, 0, 0], [0, 1, 0, 0]], dtype=np.float64) qsve = QSVE(matrix) qsigmas = qsve.top_singular_values(nprecision_bits=2, ntop=2) self.assertTrue( qsve.has_value_close_to_singular_values(qsigmas, qsve.max_error(2)))
def test_qft_dagger(self): """Visual test for the inverse QFT circuit.""" n = 5 qsve = QSVE(np.identity(4)) qreg = QuantumRegister(n) circ = QuantumCircuit(qreg) qsve._iqft(circ, qreg) print(circ)
def test_non_square_random(self): """Tests QSVE on a non-square random matrix.""" for _ in range(10): matrix = np.random.randn(2, 4) qsve = QSVE(matrix) qsigmas = qsve.top_singular_values(nprecision_bits=4, ntop=-1) self.assertTrue( qsve.has_value_close_to_singular_values( qsigmas, qsve.max_error(4)))
def test_binary_decimal_to_float(self): """Tests conversion of a string binary decimal to a float.""" for n in range(10): binary_decimal = "1" + "0" * n self.assertEqual( QSVE.binary_decimal_to_float(binary_decimal, big_endian=True), 0.5) self.assertEqual( QSVE.binary_decimal_to_float(binary_decimal, big_endian=False), 2**(-n - 1))
def test_qsve_norm(self): """Tests correctness for computing the norm.""" # Matrix to perform QSVE on matrix = np.array([[1, 0], [0, 1]], dtype=np.float64) # Create a QSVE object qsve = QSVE(matrix) # Make sure the Froebenius norm is correct self.assertTrue(np.isclose(qsve.matrix_norm(), np.sqrt(2)))
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)
def test_singular_values_identity8(self): """Tests QSVE gets close to the correct singular values for the 8 x 8 identity matrix.""" qsve = QSVE(np.identity(8)) sigma = max(qsve.singular_values_classical()) for n in range(3, 7): qsigma = qsve.top_singular_values(nprecision_bits=n, init_state_row_and_col=None, shots=50000, ntop=3) self.assertTrue(abs(sigma - qsigma[0]) < qsve.max_error(n))
def test_controlled_reflection2(self): """Basic test for controlled reflection circuit.""" regA = QuantumRegister(1) regB = QuantumRegister(2) circ = QuantumCircuit(regA, regB) init_state = np.real(self.final_state(circ)) QSVE._controlled_reflection_circuit(circ, regA, regB) final_state = np.real(self.final_state(circ)) self.assertTrue(np.allclose(init_state, final_state))
def test_unitary(self): """Tests for the unitary used for QPE.""" for dim in [2, 4]: for _ in range(50): matrix = np.random.randn(dim, dim) matrix += matrix.conj().T qsve = QSVE(matrix) unitary = qsve.unitary() self.assertTrue( np.allclose(unitary.conj().T @ unitary, np.identity(dim**2)))
def test_norm_random(self): """Tests correctness for computing the norm on random matrices.""" for _ in range(100): # Matrix to perform QSVE on matrix = np.random.rand(4, 4) matrix += matrix.conj().T # Create a QSVE object qsve = QSVE(matrix) # Make sure the Froebenius norm is correct correct = np.linalg.norm(matrix) self.assertTrue(np.isclose(qsve.matrix_norm(), correct))
def test_unitary_conjugate_evals(self): """Tests that for each eigenvalue lambda of the unitary, lambda* is also an eigenvalue, as required.""" for _ in range(100): matrix = np.random.randn(2, 2) matrix += matrix.conj().T qsve = QSVE(matrix) umat = qsve.unitary() evals, _ = np.linalg.eig(umat) for eval in evals: self.assertIn(np.conjugate(eval), evals)
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))
def test_row_norm_tree_prep_circuit(self): """Tests the state preparation circuit for the row norm tree.""" # Test matrix matrix = np.array( [[1, 1, 0, 1], [0, 1, 0, 1], [1, 1, 1, 1], [0, 1, 0, 0]], dtype=np.float64) # Make it Hermitian matrix += matrix.conj().T # Create the QSVE object qsve = QSVE(matrix) # Calculate the correct two-norms for each row of the matrix. # This vector is the state the prep circuit should make. two_norms = np.array([np.linalg.norm(row, ord=2) for row in matrix ]) / np.linalg.norm(matrix, "fro") # Get a register and circuit to prepare the row norm state in register = QuantumRegister(2) circ = QuantumCircuit(register) # Get the state preparation circuit qsve.row_norm_tree.preparation_circuit(circ, register) # Add a swap gate to get the amplitudes in a sensible order circ.swap(register[0], register[1]) # Get the final state of the circuit state = np.real(self.final_state(circ)) self.assertTrue(np.allclose(state, two_norms))
def test_shift_identity(self): """Tests shifting the identity matrix in QSVE.""" # Matrix for QSVE matrix = np.identity(2) # Get a QSVE object qsve = QSVE(matrix) # Shift the matrix qsve.shift_matrix() # Get the correct shifted matrix correct = matrix + np.linalg.norm(matrix) * np.identity(2) # Make sure the QSVE shifted matrix is correct self.assertTrue(np.allclose(qsve.matrix, correct))
def test_singular_values_random4x4(self): """Tests computing the singular values for random 4 x 4 matrices.""" for _ in range(10): matrix = np.random.randn(4, 4) matrix += matrix.conj().T qsve = QSVE(matrix) print("Matrix:") print(matrix) sigmas = qsve.singular_values_classical() print("Sigmas:", sigmas) n = 6 qsigmas = qsve.top_singular_values(nprecision_bits=n, init_state_row_and_col=None, shots=50000, ntop=4) print("QSigmas:", qsigmas) print("Max theory error:", qsve.max_error(n)) self.assertTrue( qsve.has_value_close_to_singular_values( qsigmas, qsve.max_error(n))) print("Success!\n\n")
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)
def test_prepare_singular_vector(self): """Tests preparing a singular vector in two registers.""" matrix = np.identity(2) qsve = QSVE(matrix) # Only use the real eigenvectors evecs = qsve.unitary_evecs() for vec in [evecs[0], evecs[1]]: row = QuantumRegister(1) col = QuantumRegister(1) circ = QuantumCircuit(row, col) qsve._prepare_singular_vector(np.real(vec), circ, row, col) circ.swap(row[0], col[0]) state = self.final_state(circ) self.assertTrue(np.allclose(state, np.real(vec)))
def test_create_qsve(self): """Basic test for instantiating a QSVE object.""" # Matrix to perform QSVE on matrix = np.array([[1, 0], [0, 1]], dtype=np.float64) # Create a QSVE object qsve = QSVE(matrix) # Basic checks self.assertTrue(np.allclose(qsve.matrix, matrix)) self.assertEqual(qsve.matrix_ncols, 2) self.assertEqual(qsve.matrix_nrows, 2)
def test_unitary_evals_to_matrix_singular_vals(self): """Tests QSVE.unitary() by ensuring the eigenvalues of the unitary relate to the singular values of the input matrix in the expected way. """ for _ in range(100): matrix = np.random.randn(2, 2) matrix += matrix.conj().T qsve = QSVE(matrix) sigmas = qsve.singular_values_classical(normalized=True) umat = qsve.unitary() evals, _ = np.linalg.eig(umat) qsigmas = [] for eval in evals: qsigma = qsve.unitary_eval_to_singular_value(eval) qsigma = round(qsigma, 4) if qsigma not in qsigmas: qsigmas.append(qsigma) self.assertTrue( np.allclose(sorted(qsigmas), sorted(sigmas), atol=1e-3))
def test_row_isometry_two_by_two(self): """Tests that the row isometry is indeed an isometry for random two by two matrices.""" iden = np.identity(2) for _ in range(100): # Get a Hermitian matrix matrix = np.random.randn(2, 2) matrix += matrix.conj().T # Create the QSVE object and get the row isometry qsve = QSVE(matrix) umat = qsve.row_isometry() vmat = qsve.norm_isometry() # Make sure U^dagger U = I self.assertTrue(np.allclose(umat.conj().T @ umat, iden)) self.assertTrue(np.allclose(vmat.conj().T @ vmat, iden)) # Make sure U^dagger V = V^dagger U = A / ||A||_F self.assertTrue( np.allclose(umat.conj().T @ vmat, matrix / np.linalg.norm(matrix, ord="fro"))) self.assertTrue( np.allclose(vmat.conj().T @ umat, matrix / np.linalg.norm(matrix, ord="fro")))
def test_row_norm_tree(self): """Tests creating the row norm tree for a matrix.""" # Test matrix matrix = np.array( [[1, 1, 0, 1], [0, 1, 0, 1], [1, 1, 1, 0], [0, 1, 1, 0]], dtype=np.float64) # Make it Hermitian matrix += matrix.conj().T # Create the QSVE object qsve = QSVE(matrix) # Calculate the correct two-norms for each row of the matrix two_norms = np.array([np.linalg.norm(row, ord=2) for row in matrix]) self.assertTrue(np.allclose(two_norms, qsve.row_norm_tree._values)) self.assertTrue(np.allclose(two_norms**2, qsve.row_norm_tree.leaves)) self.assertTrue( np.isclose(qsve.row_norm_tree.root, np.linalg.norm(matrix, "fro")**2))
def test_row_norm_tree_random(self): """Tests correctness of the row norm tree for random matrices.""" for _ in range(100): # Get a random matrix matrix = np.random.randn(8, 8) # Make it Hermitian matrix += matrix.conj().T # Create the QSVE object qsve = QSVE(matrix) # Calculate the correct two-norms for each row of the matrix two_norms = np.array( [np.linalg.norm(row, ord=2) for row in matrix]) self.assertTrue(np.allclose(two_norms, qsve.row_norm_tree._values)) self.assertTrue( np.allclose(two_norms**2, qsve.row_norm_tree.leaves)) self.assertTrue( np.isclose(qsve.row_norm_tree.root, np.linalg.norm(matrix, "fro")**2))
def test_shift(self): """Tests shifting an input matrix to QSVE.""" # Matrix for QSVE matrix = np.array([[1, 2], [2, 4]], dtype=np.float64) # Compute the correct norm for testing the shift norm_correct = np.linalg.norm(matrix) # Get a QSVE object qsve = QSVE(matrix) # Get the BinaryTree's (one for each row of the matrix) tree1 = deepcopy(qsve.get_tree(0)) tree2 = deepcopy(qsve.get_tree(1)) # Shift the matrix qsve.shift_matrix() # Get the correct shifted matrix correct = matrix + norm_correct * np.identity(2) # Make sure the QSVE shifted matrix is correct self.assertTrue(np.allclose(qsve.matrix, correct)) # Get the new BinaryTrees after shifting new_tree1 = qsve.get_tree(0) new_tree2 = qsve.get_tree(1) # Get the new correct tree values correct_new_tree1_values = np.array( [tree1._values[0] + norm_correct, tree1._values[1]]) correct_new_tree2_values = np.array( [tree2._values[0], tree2._values[1] + norm_correct]) # Make sure the BinaryTrees in the qsve object were updated correctly self.assertTrue( np.array_equal(new_tree1._values, correct_new_tree1_values)) self.assertTrue( np.array_equal(new_tree2._values, correct_new_tree2_values))
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