def operator_order(): """Evaluate order of operations and corresponding matmuls.""" hi = ops.Hadamard() * ops.Identity() cx = ops.Cnot(0, 1) # Make sure that the order of evaluation is correct. For example, # this simple circuit: # # |0> --- H --- o --- # | # |0> ----------X --- # # p0 p1 p2 # # Can be evaluated step wise, applying each gate to psi: psi_0 = state.zeros(2) psi_1 = hi(psi_0) psi_2 = cx(psi_1) # Or via a combined operator. Yet, the order ot the ops # has to be reversed from above picture: combined_op = (cx @ hi) combined_psi = state.zeros(2) combined_psi_2 = combined_op(combined_psi) if not psi_2.is_close(combined_psi_2): raise AssertionError('Invalid order of operators from matmul') # This can also be expressed via the function call construct: combined_f = hi(cx) combined_f_psi = state.zeros(2) combined_f_psi_2 = combined_f(combined_f_psi) if not psi_2.is_close(combined_f_psi_2): raise AssertionError('Invalid order of operators from call construct')
def test_density_to_cartesian(self): """Test density to cartesian conversion.""" q0 = state.zeros(1) rho = q0.density() x, y, z = helper.density_to_cartesian(rho) self.assertEqual(x, 0.0) self.assertEqual(y, 0.0) self.assertEqual(z, 1.0) q1 = state.ones(1) rho = q1.density() x, y, z = helper.density_to_cartesian(rho) self.assertEqual(x, 0.0) self.assertEqual(y, 0.0) self.assertEqual(z, -1.0) qh = ops.Hadamard()(q0) rho = qh.density() x, y, z = helper.density_to_cartesian(rho) self.assertTrue(math.isclose(np.real(x), 1.0, abs_tol=1e-6)) self.assertTrue(math.isclose(np.real(y), 0.0)) self.assertTrue(math.isclose(np.real(z), 0.0, abs_tol=1e-6)) qr = ops.RotationZ(90.0 * math.pi / 180.0)(qh) rho = qr.density() x, y, z = helper.density_to_cartesian(rho) self.assertTrue(math.isclose(np.real(x), 0.0, abs_tol=1e-6)) self.assertTrue(math.isclose(np.real(y), -1.0, abs_tol=1e-6)) self.assertTrue(math.isclose(np.real(z), 0.0, abs_tol=1e-6))
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 with_matmul(): psi = state.zeros(n) ident = ops.Identity(n) h = ops.Hadamard(n) psi = (ident @ h)(psi) return psi
def run_experiment(): """Run single, defined experiment for secret 11.""" psi = state.zeros(4) u = make_u() psi = ops.Hadamard(2)(psi) psi = u(psi) psi = ops.Hadamard(2)(psi) # Because of the xor patterns (Yanofski 6.64) # measurement will only find those qubit strings where # the scalar product of z (lower bits) and secret string: # <z, c> = 0 # # This should measure |00> and |11> with equal probability. # If true, than we can derive the secret string as being 11 # because f(00) = f(11) and because f(00) = f(00 ^ c) -> c = 11 # print('Measure likely states (want: pairs of 00 or 11):') for bits in helper.bitprod(4): if psi.prob(*bits) > 0.01: if (bits[0] == 0 and bits[1] == 1) or (bits[0] == 1 and bits[1] == 0): raise AssertionError('Invalid Results') print('|{}{} {}{}> = 0 : {:.2f} dot % 2: {:.2f}'.format( bits[0], bits[1], bits[2], bits[3], psi.prob(*bits), dot2(bits)))
def by_op(): psi = state.zeros(n) ident = ops.Identity(n) h = ops.Hadamard(n) psi = ident(psi) psi = h(psi) return psi
def test_rk(self): rk0 = ops.Rk(0) self.assertTrue(rk0.is_close(ops.Identity())) rk1 = ops.Rk(1) self.assertTrue(rk1.is_close(ops.PauliZ())) rk2 = ops.Rk(2) self.assertTrue(rk2.is_close(ops.Sgate())) rk3 = ops.Rk(3) self.assertTrue(rk3.is_close(ops.Tgate())) for idx in range(8): psi = state.zeros(2) psi = (ops.Rk(idx)**2 @ ops.Rk(-idx)**2)(psi) self.assertTrue(psi.is_close(state.zeros(2)))
def test_schmidt(self): psi = state.zeros(2) self.assertEqual(psi.schmidt_number([1]), 1.0) psi = state.bitstring(0, 1, 1, 0, 1, 0, 1, 1) self.assertEqual(psi.schmidt_number([1]), 1.0) psi = state.State(np.array([1.0, 1.0, 0.0, 1.0])) self.assertNotEqual(psi.schmidt_number([1]), 1.0)
def basis_kick1(): """Simple H-Cnot-H phase kick.""" psi = state.zeros(3) * state.ones(1) psi = ops.Hadamard(4)(psi) psi = ops.Cnot(2, 3)(psi, 2) psi = ops.Hadamard(4)(psi) if psi.prob(0, 0, 1, 1) < 0.9: raise AssertionError('Something is wrong with the phase kick')
def test_measure(self): psi = state.zeros(2) psi = ops.Hadamard()(psi) psi = ops.Cnot(0, 1)(psi) p0, psi2 = ops.Measure(psi, 0) self.assertTrue(math.isclose(p0, 0.5, abs_tol=1e-5)) # Measure again - now state should have collapsed. p0, _ = ops.Measure(psi2, 0) self.assertTrue(math.isclose(p0, 1.0, abs_tol=1e-6))
def test_bell(self): """Check successful entanglement by computing the schmidt_number.""" b00 = bell.bell_state(0, 0) self.assertGreater(b00.schmidt_number([1]), 1.0) self.assertTrue( b00.is_close((state.zeros(2) + state.ones(2)) / math.sqrt(2))) # Note the order is reversed from pictorials. op_exp = (ops.Cnot(0, 1) @ (ops.Hadamard() * ops.Identity())) b00_exp = op_exp(state.zeros(2)) self.assertTrue(b00.is_close(b00_exp)) b01 = bell.bell_state(0, 1) self.assertGreater(b01.schmidt_number([1]), 1.0) b10 = bell.bell_state(1, 0) self.assertGreater(b10.schmidt_number([1]), 1.0) b11 = bell.bell_state(1, 1) self.assertGreater(b11.schmidt_number([1]), 1.0)
def run_experiment(nbits: int) -> None: """Run full experiment for a given number of bits.""" c = make_c(nbits - 1) u = make_u(nbits, c) psi = state.zeros(nbits - 1) * state.ones(1) psi = ops.Hadamard(nbits)(psi) psi = u(psi) psi = ops.Hadamard(nbits)(psi) check_result(nbits, c, psi)
def run_oracle_experiment(nbits) -> None: """Run full experiment for a given number of bits.""" c = make_c(nbits - 1) f = make_oracle_f(c) u = ops.OracleUf(nbits, f) psi = state.zeros(nbits - 1) * state.ones(1) psi = ops.Hadamard(nbits)(psi) psi = u(psi) psi = ops.Hadamard(nbits)(psi) check_result(nbits, c, psi)
def test_double_hadamard(self): """Check that Hadamard is fully reversible.""" psi = state.zeros(2) psi2 = ops.Hadamard(2)(ops.Hadamard(2)(psi)) self.assertEqual(psi2.nbits, 2) self.assertTrue(psi.is_close(psi2)) combo = ops.Hadamard(2) @ ops.Hadamard(2) psi3 = combo(psi) self.assertEqual(psi3.nbits, 2) self.assertTrue(psi.is_close(psi3)) self.assertTrue(psi.density().is_pure())
def ghz_state(nbits) -> state.State: """Make a maximally entangled nbits state (GHZ State).""" # Simple construction via: # # |0> --- H --- o --------- # |0> ----------X --- o --- # |0> ----------------X --- ... # psi = state.zeros(nbits) psi = ops.Hadamard()(psi) for offset in range(nbits - 1): psi = ops.Cnot(0, 1)(psi, offset) return psi
def run_experiment(nbits, flavor): """Run full experiment for a given flavor of f().""" f = make_f(nbits - 1, flavor) u = ops.OracleUf(nbits, f) psi = (ops.Hadamard(nbits - 1)(state.zeros(nbits - 1)) * ops.Hadamard()(state.ones(1))) psi = u(psi) psi = (ops.Hadamard(nbits - 1) * ops.Identity(1))(psi) # Measure all of |0>. If all close to 1.0, f() is constant. for idx in range(nbits - 1): p0, _ = ops.Measure(psi, idx, tostate=0, collapse=False) if not math.isclose(p0, 1.0, abs_tol=1e-5): return exp_balanced return exp_constant
def test_bloch(self): psi = state.zeros(1) x, y, z = helper.density_to_cartesian(psi.density()) self.assertEqual(x, 0.0) self.assertEqual(y, 0.0) self.assertEqual(z, 1.0) psi = ops.PauliX()(psi) x, y, z = helper.density_to_cartesian(psi.density()) self.assertEqual(x, 0.0) self.assertEqual(y, 0.0) self.assertEqual(z, -1.0) psi = ops.Hadamard()(psi) x, y, z = helper.density_to_cartesian(psi.density()) self.assertTrue(math.isclose(x, -1.0, abs_tol=1e-6)) self.assertTrue(math.isclose(y, 0.0, abs_tol=1e-6)) self.assertTrue(math.isclose(z, 0.0, abs_tol=1e-6))
def test_simple_state(self): psi = state.zero self.assertEqual(psi[0], 1) self.assertEqual(psi[1], 0) psi = state.one self.assertEqual(psi[0], 0) self.assertEqual(psi[1], 1) psi = state.zeros(8) self.assertEqual(psi[0], 1) for i in range(1, 2**8 - 1): self.assertEqual(psi[i], 0) psi = state.ones(8) for i in range(0, 2**8 - 2): self.assertEqual(psi[i], 0) self.assertEqual(psi[2**8 - 1], 1) psi = state.rand(8) self.assertEqual(psi.nbits, 8)
def run_experiment(nbits): """Run single, defined experiment for secret 11.""" psi = state.zeros(nbits * 2) c = make_c(nbits) u = make_u(nbits, c) psi = ops.Hadamard(nbits)(psi) psi = u(psi) psi = ops.Hadamard(nbits)(psi) # Because of the xor patterns (Yanofski 6.64) # measurement will only find those qubit strings where # the scalar product of z (lower bits) and secret string: # <z, c> = 0 # print('Measure likely states:') for bits in helper.bitprod(nbits * 2): if psi.prob(*bits) > 0.0 and dot2(bits, nbits) < 1.0: print('|{}> = 0 : {:.2f} dot % 2: {:.2f}'.format( bits, psi.prob(*bits), dot2(bits, nbits)))
def ControlledU(idx0, idx1, u): """Control qubit at idx1 via controlling qubit at idx0.""" if idx0 == idx1: raise ValueError('Control and controlled qubit must not be equal.') p0 = Projector(state.zeros(1)) p1 = Projector(state.ones(1)) ifill = Identity(int(math.fabs(idx1 - idx0)) - 1) # space between qubits ufill = Identity()**u.nbits # 'width' of U in terms of Identity matrices if idx1 > idx0: if idx1 - idx0 > 1: op = p0 * ifill * ufill + p1 * ifill * u else: op = p0 * ufill + p1 * u else: if idx0 - idx1 > 1: op = ufill * ifill * p0 + u * ifill * p1 else: op = ufill * p0 + u * p1 return op
def basis_changes(): """Explore basis changes via Hadamard.""" # Generate [1, 0] psi = state.zeros(1) # Hadamard will result in 1/sqrt(2) [1, 1] (|+>) psi = ops.Hadamard()(psi) # Generate [0, 1] psi = state.ones(1) # Hadamard on |1> will result in 1/sqrt(2) [1, -1] (|->) psi = ops.Hadamard()(psi) # Simple PauliX will result in 1/sqrt(2) [-1, 1] psi = ops.PauliX()(psi) # But back to computational, will result in -|1>, but phases # can be ignored. psi = ops.Hadamard()(psi) if psi[1] > -0.95: raise AssertionError('Invalid Basis Changes')
def basis_changes(): """Explore basis changes via Hadamard.""" # Generate [1, 0] psi = state.zeros(1) # Hadamard will result in 1/sqrt(2) [1, 1] (|+>) psi = ops.Hadamard()(psi) # Generate [0, 1] psi = state.ones(1) # Hadamard on |1> will result in 1/sqrt(2) [1, -1] (|->) psi = ops.Hadamard()(psi) # Simple PauliX will result in 1/sqrt(2) [-1, 1] psi = ops.PauliX()(psi) # But back to computational, will result in -|1>. # Global phases can be ignored. psi = ops.Hadamard()(psi) if not np.allclose(psi[1], -1.0): raise AssertionError('Invalid basis change.')
def apply_single(): psi = state.zeros(nbits) psi = apply_single_gate(gate, qubit, psi)
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 zeros(self, n: int) -> None: self.psi = self.psi * state.zeros(n) self.global_reg = self.global_reg + n
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 with_matmul(): psi = state.zeros(nbits) op = ops.Identity(qubit) * gate * ops.Identity(nbits - qubit - 1) psi = op(psi)
def with_matrix(): psi = state.zeros(nbits) psi = ops.Hadamard(nbits)(psi)
def bench(): psi = state.zeros(nbits) for i in range(nbits): psi = apply_single_gate(h, i, psi)
def individual(): psi = state.zeros(nbits) h = ops.Hadamard() for i in range(nbits): psi = apply_single_gate(h, i, psi)