from math import acos # acos is the inverse of function cosine
from math import pi 

def angle_between_two_quantum_states(quantum_state1,quantum_state2):
    inner_product = quantum_state1[0] * quantum_state2[0] + quantum_state1[1] * quantum_state2[1]
    return acos(inner_product) 

print("function 'angle_between_two_quantum_states' is defined now, and so it can be used in the following part")

print("the angle between |0> and |1> is pi/2 (90 degree)")
print(angle_between_two_quantum_states([1,0],[0,1]),"is the angle between |0> and |1>")
print(pi/2,"is the value of pi/2")
print()

print("the angle between |0> and the quantum state [3/5,4/5] is around 0.295*pi")
print(angle_between_two_quantum_states([3/5,4/5],[1,0]),"is the angle between |0> and the quantum state [3/5,4/5]")
print(0.295*pi,"is the value of 0.295*pi")
print()

print("the angle between |0> and quantum state [-1/(2**0.5),-1/(2**0.5)] is 3pi/4 (135 degree)")
print(angle_between_two_quantum_states([-1/(2**0.5),-1/(2**0.5)],[1,0]),"is the angle between |0> and quantum state [-1/(2**0.5),-1/(2**0.5)]")
print(3*pi/4,"is the value of 3*pi/4",)
print()

#
# OPTIONAL
# 
# you may also test the function angle_between_two_quantum_states
#


from random import randrange
# randomly create a quantum state of a qubit
def random_quantum_state():
    first_entry = randrange(100)
    first_entry = first_entry/100
    first_entry = first_entry**0.5
    if randrange(2) == 0: # determine the sign of the first entry
        first_entry = -1 * first_entry
    second_entry = 1 - (first_entry**2)
    second_entry = second_entry**0.5 # the second entry cannot be nonnegative
    if randrange(2) == 0: # determine the sign
       second_entry = -1 * second_entry
    return [first_entry,second_entry]

print("function 'random_quantum_state' is defined now, and so it can be used in the following part")

from math import pi 

def find_angle_of_a_quantum_state(quantum_state):
    # find the angle between quantum_state and [1,0]
    angle = angle_between_two_quantum_states(quantum_state,[1,0])
    if quantum_state[1] < 0: # the angle is greater than pi
        angle = 2 * pi - angle
    return angle

print("function 'find_angle_of_a_quantum_state' is defined now, and so it can be used in the following part")

# find the angles of |0>, |1>, -|0>, and -|1>
print(find_angle_of_a_quantum_state([1,0]),"is the angle of |0>, which should be 0*pi=",0*pi)
print(find_angle_of_a_quantum_state([0,1]),"is the angle of |1>, which should be pi/2=",pi/2)
print(find_angle_of_a_quantum_state([-1,0]),"is the angle of -|0>, which should be pi=",pi)
print(find_angle_of_a_quantum_state([0,-1]),"is the angle of -|1>, which should be 3*pi/2=",3*pi/2)
print()

# find the angle of H|0> = [1/2**0.5,1/2**0.5]
print(find_angle_of_a_quantum_state([1/2**0.5,1/2**0.5]),"is the angle of H|0>, which should be pi/4=",pi/4)
# find the angle of [-1/2**0.5,1/2**0.5], which is pi/2 more than the previous angle
print(find_angle_of_a_quantum_state([-1/2**0.5,1/2**0.5]),"is pi/2 more than the previous angle, which should be 3*pi/4=",3*pi/4)
# find the angle of [-1/2**0.5,-1/2**0.5], which is pi/2 more than the previous angle
print(find_angle_of_a_quantum_state([-1/2**0.5,-1/2**0.5]),"is pi/2 more than the previous angle, which should be 5*pi/4=",5*pi/4)
# find the angle of [-1/2**0.5,1/2**0.5], which is pi/2 more than the previous angle
print(find_angle_of_a_quantum_state([1/2**0.5,-1/2**0.5]),"is pi/2 more than the previous angle, which should be 7*pi/4=",7*pi/4)


def visualize_quantum_states(quantum_states):
    # import the useful tool for drawing figures in pythpn
    from matplotlib.pyplot import plot, show, figure, Circle, axis, gca, annotate, arrow, text
    # import the constant pi
    from math import pi
    
    figure(figsize=(6,6), dpi=80) # size of the figure
    gca().add_patch( Circle((0,0),1,color='black',fill=False) ) # draw the circle
    # auxiliary points
    plot(-1.3,0)
    plot(1.3,0)
    plot(0,1.3)
    plot(0,-1.3)
    # axes
    arrow(0,0,1.1,0,head_width=0.04, head_length=0.08)
    arrow(0,0,-1.1,0,head_width=0.04, head_length=0.08)
    arrow(0,0,0,-1.1,head_width=0.04, head_length=0.08)
    arrow(0,0,0,1.1,head_width=0.04, head_length=0.08)
    
    # draw all quantum states

    for quantum_state in quantum_states:
        # show the quantum state as an arrow on the diagram
        state_name = quantum_state[0] # label of the quantum state 
        x_value = quantum_state[1] # amplitude of |0>
        y_value = quantum_state[2] # amplitude of |1>
        # draw the arrow
        arrow(0,0,x_value,y_value,head_width=0.04, head_length=0.04,color='blue')

         # the following code is used to write the name of quantum states
        if x_value<0: text_x_value=x_value-0.1
        else: text_x_value=x_value+0.05
        if y_value<0: text_y_value=y_value-0.1
        else: text_y_value=y_value+0.05       
        text(text_x_value,text_y_value,state_name)

    show() # show the diagram
# end of function

print("function 'visualize_quantum_states' is defined now, and so it can be used in the following part")

# define a list of three quantum states with their labels
all_quantum_states =[
    ['u',-1/2**0.5,-1/2**0.5],
    ['v',3/5,4/5],
    ['|1>',0,1]
]

# visualize all quantum states
visualize_quantum_states(all_quantum_states)

def amplitudes_of_a_quantum_state(quantum_circuit):
    # import all necessary objects and methods for quantum circuits
    from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, execute, Aer
    
     # the following code is used to get the quantum state of a quantum circuit
    job = execute(quantum_circuit,Aer.get_backend('statevector_simulator'))
    current_quantum_state=job.result().get_statevector(quantum_circuit) 
    
    # now we read the real parts of the amplitudes
    the_first_amplitude = current_quantum_state[0].real # amplitude of |0>
    the_second_amplitude = current_quantum_state[1].real # amplitude of |1>
    
    return[the_first_amplitude,the_second_amplitude]
# end of function

def reflection_game():
    # randomly construct u
    u = random_quantum_state()
    print("u is",u)

    # randomly construct v
    v = random_quantum_state()
    print("v is",v)

    print()    
    # the angle representing u uniquely
    angle_of_u = find_angle_of_a_quantum_state(u)
    print("the angle uniquely representing u is",angle_of_u,"=",angle_of_u/pi*180,"degrees")

    # the angle representing v uniquely
    angle_of_v = find_angle_of_a_quantum_state(v)
    print("the angle uniquely representing v is",angle_of_v,"=",angle_of_v/pi*180,"degrees")

    # the angle between u and v
    angle_between_u_and_v = angle_of_u-angle_of_v
    print("the angle between u and v is",angle_between_u_and_v,"=",angle_between_u_and_v/pi*180,"degrees")

    #
    #
    # we find |u>, |v>, and |uv> by writing a quantum program
    #
    #
    
    # start in |0>
    # rotate with u_angle to find |u>
    # rotate with -angle_between_u_and_v to find |v>
    # rotate with -angle_between_u_and_v once more to find the reflection of |u> over the axis |v>
    # we use ry-gatefor the rotations

    # COPY-PASTE from the previous programs

    # import all necessary objects and methods for quantum circuits
    from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, execute, Aer
    from qiskit.tools.visualization import matplotlib_circuit_drawer as drawer
    

    # we define a quantum circuit with one qubit and one bit
    qreg1 =  QuantumRegister(1) # quantum register with a single qubit
    creg1 = ClassicalRegister(1) # classical register with a single bit
    mycircuit1 = QuantumCircuit(qreg1,creg1) # quantum circuits with quantum and classical registers

    # store all quantum states with their labels
    all_quantum_states = [
        ['0',1,0] # start with |0>
    ]

    print()# print an empty line
    
    # 1)
    # already started in |0>
    # rotate with angle_of_u to find |u>
    mycircuit1.ry(2*angle_of_u,qreg1[0]) 
    
    # get the amplitudes of the current quantum state
    [x_value,y_value] = amplitudes_of_a_quantum_state(mycircuit1)

    all_quantum_states.append(['u',x_value,y_value]) # add the quantum state u

    # 2)
    # rotate with -angle_between_u_and_v to find |v>
    mycircuit1.ry(-2*angle_between_u_and_v,qreg1[0]) 

   # get the amplitudes of the current quantum state
    [x_value,y_value] = amplitudes_of_a_quantum_state(mycircuit1)

    all_quantum_states.append(['v',x_value,y_value]) # add the quantum state v


    # 3)
    # rotate with -angle_between_u_and_v once more to find the reflection of |u> over the axis |v>
    mycircuit1.ry(-2*angle_between_u_and_v,qreg1[0]) 

    # get the amplitudes of the current quantum state
    [x_value,y_value] = amplitudes_of_a_quantum_state(mycircuit1)

    all_quantum_states.append(['uv',x_value,y_value]) # add the quantum state uv

    print("all quantum states:")
    print(all_quantum_states)

    # visualize all quantum states
    visualize_quantum_states(all_quantum_states)
# end of function

print("function 'reflection game' is defined now, and so it can be used in the following part")

# game 1
reflection_game()

# game 2
reflection_game()

# game 3
reflection_game()

# game 4
reflection_game()

# game 5
reflection_game()

# games 6 - 20
for i in range(6,21):
    reflection_game()