<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Intro to Quantum Computing
2018/6/6 – WNixalo notes
pyQuil Docs
From Bit to Qubit
probabilistic bits as vector spaces
Step1:  $$Pr(i) = vec(v)^T\dot{}vec(i)$$
code
NOTE
Step2:  Applying an operation to our qubit affects the probability of each outcome
Step3:  In this case we've flipped the probability of outcome 0 into the probability of outcome 1 for our qubit. We can also investigate what happens to the state of multiple qubits. We'd expect the state of multiple qubits to grow exponentially in size, as their vectors are tensored together.
Step4:  Looking at the actual value for the state of the 2 qubits combined. The resulting dictionary of this method contains outcomes as keys and probabilities of those outcomes as values.
Step5:  Qubit Operations
Since individual qubits are 2D vectors, operations on them are 2x2 matrices. The identity matrix leaves the state vector unchanged
Step6:  pauli operators
The X gate is one of the 3 important single-qubit gates, called Pauli Operators
Step7:  Quantum programs are built by applying successive gate operations
Step8:  multi-qubit operations
Operations can be applied to composite states of multiple qubits. Example
Step9:  The CNOT gate flips (negates) the 2nd qubit IFF the 1st qubit's state is 1 (true).
The SWAP gate swaps the $\rvert01\rangle$ and $\rvert10\rangle$ states
Step10:  Summary
Step11:  On actual quantum hardware, we're unable to directly look at the wavefunction (we've been cheating up till now by looking). Instead we only have access to the C bits that are affected by measurements. This functionality is emulated by the run command.
Step12:  Both registers are zero. If we'd flipped the qubit before measurement we'd get
Step13:  These are deterministic measurements. We'll get the same result from making them multiple times
Step14:  classical/quantum interaction
In general, measurements can affect the quantum state. They act like projections onto the outcome basis states. To show this, we'll introduce a new single-qubit gate
Step15:  In a sense, this Q bit really is a random variable representing a coin. In fact there's a continuous family of states of form
Step16:  pyQuil lets us look at the wavefunction after a measurement as well
Step17:  Measurement has an effect on the quantum state independent of what's stored classically. Observation of binary state (where previously it was probabilistic) is referred to as collapse of the wavefunction. Mathematically, the wavefunction is being projected onto the vector of the obtained outcome and subsequently rescaled to unit norm.
Step18:  The above program prepared entanglement because, even though there're random outcomes after every measurement, both qubits are in the same state. They are either both $\rvert0\rangle$ or both $\rvert1\rangle$. This special kind of correlation is part of what makes quantum mechanics so unique and powerful.
classical control
There are ways of introducing C control of Q programs. For example, we can use the state of classical bits to determine what Q operations to run
Step19:  A [1] here means that qubit 7 was indeed flipped
<ASSISTANT_TASK:>
Python Code:
%matplotlib inline
%reload_ext autoreload
%autoreload 2
import numpy as np
import matplotlib.pyplot as plt
# http://pyquil.readthedocs.io/en/latest/intro.html
outcome_0 = np.array([1.0, 0.0]) # orthonorm basis vecs
outcome_1 = np.array([0.0, 1.0])
a = 0.75
b = 0.25
# vec(v) = a*vec(0) + b*vec(1)
prob_bit = a*outcome_0 + b*outcome_1
X,Y = prob_bit
fig,ax = plt.subplots()
ax.quiver(X,Y, angles='xy', scale_units='xy', scale=1)
ax.set_xlim([0,1]); ax.set_ylim([0,1])
plt.draw(); plt.show()
Explanation: Intro to Quantum Computing
2018/6/6 – WNixalo notes
pyQuil Docs
From Bit to Qubit
probabilistic bits as vector spaces
End of explanation
# imports for pyQuil
import numpy as np
from pyquil.quil import Program
from pyquil.api import QVMConnection
quantum_simulator = QVMConnection()
# pyQuil is based around operations (or gates) so we'll start with the most 
# basic one: the identity oepration, I. I takes 1 argument: the index 
# of the qubit that it should be applied to.
from pyquil.gates import I
# Make a quantum program allocating 1 qubut (qubit #0) and doing nothing to it
p = Program(I(0))
# Quantum statse are called wavefunctions, historically.
# We can run this basic program on our connection to the simulator.
# This call will return the state of our qubits after we run the program p.
# This api call returns a tuple, but we'll ignore the scond value for now.
wavefunction = quantum_simulator.wavefunction(p)
# wavefunction is a Wavefunction object that stores a quantum state as a list of amplitudes
alpha, beta = wavefunction
print(f"Qubit in state α={alpha} and β={beta}")
print(f"Measurement probability of qubit in outcome 0 is {abs(alpha)**2}")
print(f"Measurement probability of qubit in outcome 1 is {abs(beta)**2}")
Explanation: $$Pr(i) = vec(v)^T\dot{}vec(i)$$
code
NOTE: pyQuil must be installed and a Forest API connection set up.
End of explanation
def print_results(coeffs):
    alpha,beta = coeffs
    print(f"Qubit in state α={alpha} and β={beta}")
    print(f"Measurement probability of qubit in outcome 0 is {abs(alpha)**2}")
    print(f"Measurement probability of qubit in outcome 1 is {abs(beta)**2}")
# We can import the qubit "flip" operation, called X, and see what it does.
# We'll learn more about this operation in the next section.
from pyquil.gates import X
p = Program(X(0))
wavefunc = quantum_simulator.wavefunction(p)
alpha, beta = wavefunc
print_results((alpha,beta))
Explanation: Applying an operation to our qubit affects the probability of each outcome:
End of explanation
# Multiple qubits also produce the expected scaling of the state.
p = Program(I(0), I(1))
wavefunction = quantum_simulator.wavefunction(p)
print(f"Quantum state is of dim: {len(wavefunction.amplitudes)}")
p = Program(I(0), I(1), I(2), I(3))
wavefunction = quantum_simulator.wavefunction(p)
print(f'Quantum state is of dim: {len(wavefunction.amplitudes)}')
p = Program()
for x in range(10):
    p += I(x)
wavefunction = quantum_simulator.wavefunction(p)
print(f"Quantum state is of dim: {len(wavefunction.amplitudes)}")
Explanation: In this case we've flipped the probability of outcome 0 into the probability of outcome 1 for our qubit. We can also investigate what happens to the state of multiple qubits. We'd expect the state of multiple qubits to grow exponentially in size, as their vectors are tensored together.
End of explanation
# wavefunction(Program) returns a coefficient array that corresponds to outcomes in the following order
wavefunction = quantum_simulator.wavefunction(Program(I(0), I(1)))
print(wavefunction.get_outcome_probs())
Explanation: Looking at the actual value for the state of the 2 qubits combined. The resulting dictionary of this method contains outcomes as keys and probabilities of those outcomes as values.
End of explanation
p = Program(I(0))
print(quantum_simulator.wavefunction(p))
Explanation: Qubit Operations
Since individual qubits are 2D vectors, operations on them are 2x2 matrices. The identity matrix leaves the state vector unchanged:
\begin{split}I = \left(\begin{matrix}
1 & 0\
0 & 1
\end{matrix}\right)\end{split}
so the program that applies this operation to the zero state is just:
\begin{split} I\,\rvert\,0\rangle = \left(\begin{matrix}
1 & 0\
0 & 1
\end{matrix}\right)\left(\begin{matrix}
1 \
0
\end{matrix}\right) = \left(\begin{matrix}
1 \
0
\end{matrix}\right) = \rvert\,0\rangle\end{split}
End of explanation
def print_results(p_optr, state, careflip=True):
    prob = 1.
    if careflip: bitflip = int(wavefunction.get_outcome_probs()[str(state)]==prob)
    print(f"{p_optr}|{state}> = {wavefunction}")
    print(f"Outcome probabilities: {wavefunction.get_outcome_probs()}")
    if careflip: 
        result = ("looks like a bit flip","state looks unchanged")[bitflip]
        print(f"This {result}\n")
from pyquil.gates import X, Y, Z
p = Program(X(0))
wavefunction = quantum_simulator.wavefunction(p)
print_results("X", 0)
p = Program(Y(0))
wavefunction = quantum_simulator.wavefunction(p)
print_results("Y", 0)
p = Program(Z(0))
wavefunction = quantum_simulator.wavefunction(p)
print_results("Z", 0)
Explanation: pauli operators
The X gate is one of the 3 important single-qubit gates, called Pauli Operators:
\begin{split}X = \left(\begin{matrix}
0 & 1\
1 & 0
\end{matrix}\right)
\qquad
Y = \left(\begin{matrix}
0 & -i\
i & 0
\end{matrix}\right)
\qquad
Z = \left(\begin{matrix}
1 & 0\
0 & -1
\end{matrix}\right)\end{split}
End of explanation
# Composing qubit operations is the same as multiplying matrices sequentially
p = Program(X(0), Y(0), Z(0))
wavefunction = quantum_simulator.wavefunction(p)
print_results("ZYX", 0, careflip=False)
Explanation: Quantum programs are built by applying successive gate operations:
End of explanation
from pyquil.gates import CNOT
def print_cnot(qbit_states):
    print(f"CNOT|{qbit_states} = {wavefunction}")
    print(f"Outcome probabilities\n{wavefunction.get_outcome_probs()}\n")
p = Program(CNOT(0, 1)) # input qbit states: 0,0
wavefunction = quantum_simulator.wavefunction(p)
print_cnot("00")
p = Program(X(0), CNOT(0, 1)) # flip qbit 0
wavefunction = quantum_simulator.wavefunction(p)
print_cnot("01")
p = Program(X(1), CNOT(0, 1)) # flip qbit 1
wavefunction = quantum_simulator.wavefunction(p)
print_cnot("10")
p = Program(X(0), X(1), CNOT(0, 1)) # flip qbits 0,1
wavefunction = quantum_simulator.wavefunction(p)
print_cnot("11")
Explanation: multi-qubit operations
Operations can be applied to composite states of multiple qubits. Example: the controlled-NOT (CNOT) gate that works on 2 qubits:
\begin{split}CNOT = \left(\begin{matrix}
1 & 0 & 0 & 0 \
0 & 1 & 0 & 0 \
0 & 0 & 0 & 1 \
0 & 0 & 1 & 0 \
\end{matrix}\right)\end{split}
End of explanation
from pyquil.gates import SWAP
p = Program(X(0), SWAP(0,1))
wavefunction = quantum_simulator.wavefunction(p)
print(f"SWAP|01> = {wavefunction}")
print(f"Outcome probabilities\n{wavefunction.get_outcome_probs()}")
Explanation: The CNOT gate flips (negates) the 2nd qubit IFF the 1st qubit's state is 1 (true).
The SWAP gate swaps the $\rvert01\rangle$ and $\rvert10\rangle$ states:
\begin{split}SWAP = \left(\begin{matrix}
1 & 0 & 0 & 0 \
0 & 0 & 1 & 0 \
0 & 1 & 0 & 0 \
0 & 0 & 0 & 1 \
\end{matrix}\right)\end{split}
End of explanation
# Createa a program that stores the outcome of measuring qubit #0 into 
# classical register [0]
classical_register_index = 0
p = Program(I(0)).measure(0, classical_register_index)
Explanation: Summary: quantum computing operations are composed of a series of complex matrices applied to complex vectors. These matrices must be unitary (their complex conjugate transpose equals their inverse) because the overall probability of all outcomes must sum to 1.
The Quantum Abstract Machine
The programming model that underlies Quil is a hybrid–classical model in which $N$ qubits interact with $M$ classical bits:
<img src="http://pyquil.readthedocs.io/en/latest/_images/qam.png" >
Quil whitepaper.
Quantum & Classical (Q/C) bits come with a defined gate set defining which gate operations can be applied to which qubits. The fixed gate set represents the limitations on which gates can be applied by different kinds of quantum computing hardware.
qubit measurements
Measurements have 2 effects:
1. project the state vector onto 1 of the basic outcomes.
2. (optional) store the outcome of the measurement in a C bit.
End of explanation
# Choose which classical registers to look in at the end of the computation
classical_regs = [0,1]
print(quantum_simulator.run(p, classical_regs))
Explanation: On actual quantum hardware, we're unable to directly look at the wavefunction (we've been cheating up till now by looking). Instead we only have access to the C bits that are affected by measurements. This functionality is emulated by the run command.
End of explanation
p = Program(X(0)) # Flip the qubit
p.measure(0, classical_register_index) # Measure the qubit
classical_regs = [0,1]
print(quantum_simulator.run(p, classical_regs))
Explanation: Both registers are zero. If we'd flipped the qubit before measurement we'd get:
End of explanation
p = Program(X(0))
p.measure(0, classical_register_index)
classical_regs = [0]
trials = 10
print(quantum_simulator.run(p, classical_regs, trials))
Explanation: These are deterministic measurements. We'll get the same result from making them multiple times:
End of explanation
from pyquil.gates import H
# The Hadamard produces a superposition state
coin_program = Program(H(0))
wavefunction = quantum_simulator.wavefunction(coin_program)
print(f"H|0> = {wavefunction}")
print(f"Outcome probs\n{wavefunction.get_outcome_probs()}")
Explanation: classical/quantum interaction
In general, measurements can affect the quantum state. They act like projections onto the outcome basis states. To show this, we'll introduce a new single-qubit gate: the Hadamard gate. Matrix form:
\begin{split}H = \frac{1}{\sqrt{2}}\left(\begin{matrix}
1 & 1\
1 & -1
\end{matrix}\right)\end{split}
End of explanation
# Introduce measurement
classical_reg = 0
coin_program = Program(H(0)).measure(0, classical_reg)
trials = 10
# We probabilistic results of about half 1's and half 0's
print(quantum_simulator.run(coin_program, [0], trials))
Explanation: In a sense, this Q bit really is a random variable representing a coin. In fact there's a continuous family of states of form:
$$\frac{1}{\sqrt{2}}\left(\rvert\,0\rangle + e^{i\theta}\rvert\,1\rangle\right)$$
represents the outcomes of an unbiased coin. Being able to work w/ all these different new states is part of what gives quantum computing extra power wrt C bits.
End of explanation
clasical_reg = 0
coin_program = Program(H(0))
print(f"Before measurement: H|0> = {quantum_simulator.wavefunction(coin_program)}")
coin_program.measure(0, classical_reg)
for x in range(5):
    print(f"Afer meas: {quantum_simulator.wavefunction(coin_program)}")
Explanation: pyQuil lets us look at the wavefunction after a measurement as well:
End of explanation
# This happens with bigger systems too
classical_reg = 0
# This program prepares something called a Bell state
# (a special kind of 'entangled state')
bell_program = Program(H(0), CNOT(0,1))
wavefunction = quantum_simulator.wavefunction(bell_program)
print(f"Before meas: Bell state = {wavefunction}")
bell_program.measure(0, clasical_reg)
for x in range(5):
    wavefunction = quantum_simulator.wavefunction(bell_program)
    print(f'After meas: {wavefunction.get_outcome_probs()}')
Explanation: Measurement has an effect on the quantum state independent of what's stored classically. Observation of binary state (where previously it was probabilistic) is referred to as collapse of the wavefunction. Mathematically, the wavefunction is being projected onto the vector of the obtained outcome and subsequently rescaled to unit norm.
End of explanation
true_branch = Program(X(7)) #  if branch
false_branch = Program(I(7)) # else branch
# Branch on classical reg [1]
p = Program(X(0)).measure(0,1).if_then(1, true_branch, false_branch)
# Measure qubit #7 into classical register [7]
p.measure(7,7)
# Run and check register [7]
print(quantum_simulator.run(p, [7]))
Explanation: The above program prepared entanglement because, even though there're random outcomes after every measurement, both qubits are in the same state. They are either both $\rvert0\rangle$ or both $\rvert1\rangle$. This special kind of correlation is part of what makes quantum mechanics so unique and powerful.
classical control
There are ways of introducing C control of Q programs. For example, we can use the state of classical bits to determine what Q operations to run:
End of explanation
inside_loop = Program(H(0)).measure(0,1)
p = Program().inst(X(0)).while_do(1, inside_loop)
# Run and check register [1]
print(quantum_simulator.run(p, [1]))
Explanation: A [1] here means that qubit 7 was indeed flipped:
<img src="http://pyquil.readthedocs.io/en/latest/_images/branch.png" >
example: the probabilistic halting problem
A fun example is to create a program that has an exponentially increasing chance of halting... but that may run forever!
End of explanation
<END_TASK>