def run_experiment(nbits, t=8): """Run single phase estimation experiment.""" # Make a unitary and find Eigen value/vector to estimate. # umat = scipy.stats.unitary_group.rvs(2**nbits) eigvals, eigvecs = np.linalg.eig(umat) u = ops.Operator(umat) # Pick Eigenvalue 'eigen_index' (any Eigenvalue / Eigenvector pair will work). eigen_index = 1 phi = np.real(np.log(eigvals[eigen_index]) / (2j * np.pi)) if phi < 0: phi += 1 # Make state + circuit to estimate phi. # Pick Eigenvector 'eigen_index' to math the Eigenvalue. psi = state.zeros(t) * state.State(eigvecs[:, eigen_index]) psi = phase1(psi, u, t) psi = ops.Qft(t).adjoint()(psi) # Find state with highest measurement probability and show results. # maxbits, maxprob = psi.maxprob() phi_estimate = sum(maxbits[i] * 2**(-i - 1) for i in range(t)) delta = abs(phi - phi_estimate) print('Phase : {:.4f}'.format(phi)) print('Estimate: {:.4f} delta: {:.4f} probability: {:5.2f}%'.format( phi_estimate, delta, maxprob * 100.0)) if delta > 0.02 and phi_estimate < 0.98: print('*** Warning: Delta is large')
def graph_to_adjacency(n: int, nodes: List[int]) -> ops.Operator: """Compute adjacency matrix from graph.""" op = np.zeros((n, n)) for node in nodes: op[node[0], node[1]] = node[2] op[node[1], node[0]] = node[2] return ops.Operator(op)
def graph_to_hamiltonian(n: int, nodes: List[int]) -> ops.Operator: """Compute Hamiltonian matrix from graph.""" H = np.zeros((2**n, 2**n)) for node in nodes: idx1 = node[0] idx2 = node[1] if idx1 > idx2: idx1, idx2 = idx2, idx1 op = 1.0 for _ in range(idx1): op = op * ops.Identity() op = op * (node[2] * ops.PauliZ()) for _ in range(idx1 + 1, idx2): op = op * ops.Identity() op = op * (node[2] * ops.PauliZ()) for _ in range(idx2 + 1, n): op = op * ops.Identity() H = H + op return ops.Operator(H)
def run_experiment(nbits, solutions) -> None: """Run full experiment for a given flavor of f().""" # Note that op_zero multiplies the diagonal elements of the operator by -1, # except for element [0][0]. This can be interpreted as "rotating around # the |00..)>" state. More pragmatically, multiplying this op_zero with # a Hadamard from the left and right gives a matrix of this form: # # 2/N-1 2/N 2/N ... 2/N # 2/N 2N-1 2/N ... 2/N # 2/N 2/N 2/N-1 ... 2/N # 2/N 2/N 2/N ... 2/N-1 # # Multiplying this matrix with a state vector computes exactly: # 2u - c_x # for every vector element c_x, with u being the mean over the # state vector. This is the defintion of inversion about the mean. # zero_projector = np.zeros((2**nbits, 2**nbits)) zero_projector[0, 0] = 1 op_zero = ops.Operator(zero_projector) # Make f and Uf. Note: # We reserve space for an ancilla 'y', which is unused in # Grover's algorithm. This allows reuse of the Deutsch Uf builder. # # We use the Oracle construction for convenience. It is rather # slow (full matrix) for larger qubit counts. Once can construct # a 'regular' function for the grover search algorithms, but this # function is different for each bitstring and that quickly gets # confusing. # f = make_f(nbits, solutions) uf = ops.OracleUf(nbits+1, f) # Build state with 1 ancilla of |1>. # psi = state.zeros(nbits) * state.ones(1) for i in range(nbits + 1): psi.apply1(ops.Hadamard(), i) # Build Grover operator, note Id() for the ancilla. # The Grover operator is the combination of: # - phase inversion via the u unitary # - inversion about the mean (see matrix above) # hn = ops.Hadamard(nbits) reflection = op_zero * 2.0 - ops.Identity(nbits) inversion = hn(reflection(hn)) * ops.Identity() grover = inversion(uf) # Number of Grover iterations # # There are at least 2 ways to compute the required number of iterations. # # 1) Most text books, eg., page 157 of Kaye, Laflamme and Mosca, find # that the highest probability of finding the right results occurs # after pi/4 sqrt(n) rotations. # # 2) For grover specifically, with 1 solution the iteration count is: # int(math.pi / 4 * math.sqrt(n)) # # 3) For grover with multiple solutions: # int(math.pi / 4 * math.sqrt(n / solutions)) # # 4) For amplitude amplification, it's the probability of good # solutions, which is trivial with the Grover equal # superposition here: # int(math.sqrt(n / solutions)) # iterations = int(math.pi / 4 * math.sqrt(2**nbits / solutions)) for _ in range(iterations): psi = grover(psi) # Measurement - pick element with higher probability. # # Note: We constructed the Oracle with n+1 qubits, to allow # for the 'xor-ancillary'. To check the result, we need to # ignore this ancilla. # maxbits, maxprob = psi.maxprob() result = f(maxbits[:-1]) print('Got f({}) = {}, want: 1, #: {:2d}, p: {:6.4f}' .format(maxbits[:-1], result, solutions, maxprob)) if result != 1: raise AssertionError('something went wrong, measured invalid state')
def diagonalize(U): _, V = np.linalg.eig(U) return ops.Operator(V)
def spectral_decomp(ndim: int): """Implement and verify spectral decomposition theorem.""" # The spectral theorem says that a Hermitian matrix can be written as # the sum over eigenvalues lambda_i and eigenvectors u_i, as: # # A = sum_i {lambda_i * |u_i><u_i|} # # Let's check by example... # Create random unitary. It will be unitary, but not Hermitian. # u = scipy.stats.unitary_group.rvs(ndim) umat = ops.Operator(u) # Make it Hermitian by computing: # A = 1/2 * (A + A^*) # # This makes A Hermitian (but no longer unitary). # hmat = 0.5 * (umat + umat.adjoint()) if not np.allclose(hmat, hmat.adjoint()): raise AssertionError('Something is wrong, created non-Hermitian.') # Compute eigenvalues and vectors. # # Note the Python way to extract column c from # a matrix v as v[:, c]: # # w : array of eigenvalues # v[:, i] : eigenvector corresponding to w[i] # w, v = np.linalg.eig(hmat) # Check that the eigenvalues are real. # for i in range(ndim): if not np.allclose(w[i].imag, 0.0): raise AssertionError('Found non-real eigenvalue.') # Check that the eigenvectors are orthogonal. # for i in range(ndim): for j in range(i + 1, ndim): dot = np.dot(v[:, i], v[:, j].adjoint()) if not np.allclose(dot, 0.0, atol=1e-5): raise AssertionError('Invalid, non-orthogonal basis found') # Check that eigenvectors are orthonormal. for i in range(ndim): dot = np.dot(v[:, i], v[:, i].adjoint()) if not np.allclose(dot, 1.0, atol=1e-5): raise AssertionError('Found non-orthonormal basis vectors') # Construct a matrix following the spectral theorem and # check equivalance. # # A = sum_i {lambda_i * |u_i><u_i|} # x = np.matrix(np.zeros((ndim, ndim))) for i in range(ndim): x = x + w[i] * np.outer(v[:, i], v[:, i].adjoint()) if not np.allclose(hmat, x, atol=1e-5): raise AssertionError('Spectral decomp doesn\'t seem to work.') # Can we use this elegant spectral decomposition to compute the # the inverse of a matrix? # # Let's compute the inverse by using only the inverse of the eigenvalues, # as (1 / eigenvalue), but keep the same eigenvectors otherwise: # x = np.matrix(np.zeros((ndim, ndim))) for i in range(ndim): x = x + 1 / w[i] * np.outer(v[:, i], v[:, i].adjoint()) if not np.allclose(np.linalg.inv(hmat), x, atol=1e-5): raise AssertionError('Inverse computation doesn\'t seem to work.')
def run_experiment(nbits) -> None: """Run full experiment for a given flavor of f().""" hn = ops.Hadamard(nbits) h = ops.Hadamard() n = 2**nbits # Note that op_zero multiplies the diagonal elements of the operator by -1, # except for element [0][0]. This can be interpreted as "rotating around # the |00..)>" state. More pragmatically, multiplying this op_zero with # a Hadamard from the left and right gives a matrix of this form: # # 2/N-1 2/N 2/N ... 2/N # 2/N 2N-1 2/N ... 2/N # 2/N 2/N 2/N-1 ... 2/N # 2/N 2/N 2/N ... 2/N-1 # # Multiplying this matrix with a state vector computes exactly: # 2u - c_x # for every vector element c_x, with u being the mean over the # state vector. This is the defintion of inversion about the mean. # zero_projector = np.zeros((n, n)) zero_projector[0, 0] = 1 op_zero = ops.Operator(zero_projector) # Make f and Uf. Note: We reserve space for an ancilla 'y', which is unused # in Grover's algorithm. This allows reuse of the Deutsch Uf builder. # # We use the Oracle construction for convenience. It is rather slow (full # matrix) for larger qubit counts. Once can construct a 'regular' function # for the grover search algorithms, but this function is different for # each bitstring and that quickly gets quite confusing. # # Good examples for 2-qubit and 3-qubit functions can be found here: # https://qiskit.org/textbook/ch-algorithms/grover.html#3qubits # f = make_f(nbits) u = ops.OracleUf(nbits + 1, f) # Build state with 1 ancilla of |1>. # psi = state.zeros(nbits) * state.ones(1) for i in range(nbits + 1): psi.apply(h, i) # Build Grover operator, note Id() for the ancilla. # The Grover operator is the combination of: # - phase inversion via the u unitary # - inversion about the mean (see matrix above) # reflection = op_zero * 2.0 - ops.Identity(nbits) inversion = hn(reflection(hn)) * ops.Identity() grover = inversion(u) # Number of Grover iterations # # There are at least 2 ways to compute the required number of iterations. # # 1) Most text books, eg., page 157 of Kaye, Laflamme and Mosca, find # that the highest probability of finding the right results occurs # after pi/4 sqrt(n) rotations. # # 2) I found this computation in qcircuits: # angle_to_rotate = np.arccos(np.sqrt(1 / n)) # rotation_angle = 2 * np.arcsin(np.sqrt(1 / n)) # iterations = int(round(angle_to_rotate / rotation_angle)) # # Both produce identical results. # iterations = int(math.pi / 4 * math.sqrt(n)) for _ in range(iterations): psi = grover(psi) # Measurement - pick element with higher probability. # # Note: We constructed the Oracle with n+1 qubits, to allow # for the 'xor-ancillary'. To check the result, we need to # ignore this ancilla. # maxbits, maxprobs = psi.maxprob() result = f(maxbits[:-1]) print(f'Got f({maxbits[:-1]}) = {result}, want: 1') if result != 1: raise AssertionError('something went wrong, measured invalid state')
def run_experiment(nbits_phase, nbits_grover, solutions) -> None: """Run full experiment for a given number of solutions.""" # Building the Grover Operator, see grover.py n = 2**nbits_grover zero_projector = np.zeros((n, n)) zero_projector[0, 0] = 1 op_zero = ops.Operator(zero_projector) f = make_f(nbits_grover, solutions) u = ops.OracleUf(nbits_grover + 1, f) # The state for the counting algorithm. # We reserve nbits for the phase estimation. # We also reserve nbits for the Oracle. # These numbers could be adjusted to achieve better # accuracy. Yet, this keeps the code a little bit simpler, # while trading off a few off-by-1 estimation errors. # # We also add the |1> for the Oracle. # psi = (state.zeros(nbits_phase) * state.zeros(nbits_grover) * state.ones(1)) # Apply Hadamard to all the qubits. for i in range(nbits_phase + nbits_grover + 1): psi.apply(ops.Hadamard(), i) # Construct the Grover Operator. reflection = op_zero * 2.0 - ops.Identity(nbits_grover) hn = ops.Hadamard(nbits_grover) inversion = hn(reflection(hn)) * ops.Identity() grover = inversion(u) # Now that we have the Grover Operator, we have to perform # phase estimation. This loop is a copy from phase_estimation.py # with more comments there. # for idx, inv in enumerate(range(nbits_phase - 1, -1, -1)): u2 = grover for _ in range(idx): u2 = u2(u2) psi = ops.ControlledU(inv, nbits_phase, u2)(psi, inv) # Reverse QFT gives us the phase as a fraction of 2*Pi psi = ops.Qft(nbits_phase).adjoint()(psi) # Get the state with highest probability and compute the phase # as a binary fraction. Note that the probability increases # as M, the number of solutions, gets closer and closer to N, # the total mnumber of states. maxbits, maxprob = psi.maxprob() phi_estimate = sum(maxbits[i] * 2**(-i - 1) for i in range(nbits_phase)) # We know that after phase estimation, this holds: # # sin(phi/2) = sqrt(M/N) # M = N * sin(phi/2)^2 # # Hence we can compute M. We keep the result to 2 digit to visualize # the errors. Note that the phi_estimate is a fraction of 2*PI, hence # the 1/2 in above formula cancels out against the 2 and we compute: M = round(n * math.sin(phi_estimate * math.pi)**2, 2) print('Estimate: {:.4f} prob: {:5.2f}% --> M: {:5.2f}, want: {:2d}'.format( phi_estimate, maxprob * 100.0, M, solutions))