import numpy as np
#import matplotlib as plt
import random
from matplotlib import pyplot as plt
# Importing standard Qiskit libraries
from qiskit import *
from qiskit.tools import *
from qiskit.tools.jupyter import *
from qiskit.visualization import *
from ibm_quantum_widgets import *
from qiskit.providers.aer import QasmSimulator
from qiskit.tools.visualization import plot_histogram
from qiskit import IBMQ


# IBMQ.save_account(TOKEN)
IBMQ.load_account() # Load account from disk
provider = IBMQ.get_provider(hub='ibm-q', group='open', project='main')
provider.backends()

#Load the necessary backend (unitary simulator)
backend = Aer.get_backend('unitary_simulator')
#make the quantum circuit
qc = QuantumCircuit(1,1)
qc.x(0)  # add the X gate 
print(qc) #print the circuit

#send the job to the simulator to retrieve the unitary 
job = execute(qc, backend, shots=1024)
result = job.result()
unitary = result.get_unitary(qc) #this gets the matrix of the unitary gate

print('X=\n',unitary) #print out the matrix of the X gate


#similary now use HZH
qc1 = QuantumCircuit(1,1)
qc1.h(0)
qc1.z(0)
qc1.h(0)
print(qc1)
 
#send the job to the simulator to retrieve the unitary 
job = execute(qc1, backend, shots=1024)
result = job.result()
unitary = result.get_unitary(qc1, decimals=2) #this gets the matrix of the unitary gate
print('thus HZH=X=\n',unitary) #print out the matrix of the HZH gate

#similary now use CNOT gate (with control at q_0 and target at q_1 qubits respectively)
qc2 = QuantumCircuit(2,2)
qc2.cnot(0,1)
print(qc2)
 
#send the job to the simulator to retrieve the unitary 
job = execute(qc2, backend, shots=1024)
result = job.result()
unitary = result.get_unitary(qc2, decimals=2) #this gets the matrix of the unitary gate
print('CNOT_01=\n',unitary)  #print out the matrix of the CNOT_01 gate

#similary now use CNOT gate (with control at q_1 and target at q_0 qubits respectively) with Hadamard gate
#make the quantum circuit
qc3 = QuantumCircuit(2,2)
qc3.h(0)
qc3.h(1)
qc3.cnot(1,0)
qc3.h(0)
qc3.h(1)
print(qc3) #print the circuit
 
#send the job to the simulator to retrieve the unitary 
job = execute(qc3, backend, shots=1024)
result = job.result()
unitary = result.get_unitary(qc3, decimals=2) #this gets the matrix of the unitary gate
print('equivalent CNOT_01=\n',unitary)  #print out the matrix of the CNOT_01 gate equivalence

#similary now use SWAP gate
qc4 = QuantumCircuit(2,2)
qc4.swap(0,1)
print(qc4)
 
#send the job to the simulator to retrieve the unitary 
job = execute(qc4, backend, shots=1024)
result = job.result()
unitary = result.get_unitary(qc4, decimals=2) #this gets the matrix of the unitary gate
print('SWAP=\n',unitary)  #print out the matrix of the SWAP gate

#similary now use combination of CNOT gates to show SWAP equivalence
#make the quantum circuit
qc5 = QuantumCircuit(2,2)
qc5.cx(0,1)
qc5.cx(1,0)
qc5.cx(0,1)
print(qc5) #print the circuit
 
#send the job to the simulator to retrieve the unitary 
job = execute(qc5, backend, shots=1024)
result = job.result()
unitary = result.get_unitary(qc5, decimals=2) #this gets the matrix of the unitary gate
print('equivalent SWAP=\n',unitary)  #print out the matrix of the SWAP gate equivalence

#make the increment circuit 
# define the required registers
p_1 = QuantumRegister(1, name='p1')
a_1 = QuantumRegister(1, name='a1')

p_2 = QuantumRegister(1, name='p2')
a_2 = QuantumRegister(1, name='a2')

p_3 = QuantumRegister(1, name='p3')
a_3 = QuantumRegister(1, name='a3')

p_4 = QuantumRegister(1, name='p4')

#inout the registers into the quantum circuit
incq = QuantumCircuit(p_1, a_1, p_2, a_2, p_3, a_3, p_4)
incq.cx(0,1)
incq.ccx(1,2,3)
incq.ccx(3,4,5)
incq.cx(5,6)
incq.ccx(3,4,5)
incq.cx(3,4)
incq.ccx(1,2,3)
incq.cx(1,2)
incq.cx(0,1)
incq.x(0)

#draw the incrementer circuit
incq.draw()

inc_gate = incq.to_gate(label=' (inc)')  #turn the above increment circuit to a gate
#make a new circuit
new_inc = QuantumCircuit(7)
new_inc.append(inc_gate, [0,1,2,3,4,5,6]) #append the gate in to the circuit
new_inc.draw() #increment gate is embedded in to the circuit now

# Define the required registers
p_1 = QuantumRegister(1, name='p1')
a_1 = QuantumRegister(1, name='a1')

p_2 = QuantumRegister(1, name='p2')
a_2 = QuantumRegister(1, name='a2')

p_3 = QuantumRegister(1, name='p3')
a_3 = QuantumRegister(1, name='a3')

p_4 = QuantumRegister(1, name='p4')

# Input the registers into the quantum circuit
decq = QuantumCircuit(p_1, a_1, p_2, a_2, p_3, a_3, p_4)

# Apply the operations in reverse order to "uncompute" the increment circuit
decq.x(0)
decq.cx(0, 1)
decq.cx(1, 2)
decq.ccx(1, 2, 3)
decq.cx(3, 4)
decq.ccx(3, 4, 5)
decq.cx(5, 6)
decq.ccx(3,4,5)
decq.ccx(1, 2, 3)
decq.cx(0,1)


# Draw the decrementer circuit
decq.draw()


dec_gate = decq.to_gate(label=' (dec)')  #turn the above decrementer circuit to a gate
#make a new circuit
new_dec = QuantumCircuit(7)
new_dec.append(dec_gate, [0,1,2,3,4,5,6]) #append the gate in to the circuit
new_dec.draw() #decrement gate is embedded into the circuit now

#make the controlled L gate first
L = new_inc.to_gate(label= ' (c-inc)').control(1,ctrl_state='0')

#make the controlled R gate 
R = new_dec.to_gate(label= ' (c-dec)').control(1)

#design the circuit to hold the above to gates and make a basic walker (condition-shift)
shift= QuantumCircuit(8)
shift.append(L, [0,1,2,3,4,5,6,7]) #input the L gate
#shift.x(0)
shift.append(R, [0,1,2,3,4,5,6,7]) #input the R gate
shift.draw('mpl')




#make the shift operator gate by converting the above inc-dec circuit to gate
shift_gate = shift.to_gate( label= ' S = shift operator')

#design the step circuit with coin 
shift_op = QuantumCircuit(8)
shift_op.h(0) #this is the coin
shift_op.append(shift_gate, [0,1,2,3,4,5,6,7]) #append the shift gate 
shift_op.draw('mpl') #the step circuit is ready

#convert the step circuit to gate
step_gate = shift_op.to_gate(label='step_gate')

#design our first step circuit for walk
step_op = QuantumCircuit(8)
step_op.append(step_gate,[0,1,2,3,4,5,6,7])
step_op.draw() #walk-step operator gate is now ready 

#prepare the random walk step circuit 
qWalk = QuantumCircuit(8,4)
#inout the above initial state|13>
qWalk.x(1)
qWalk.x(3)
n=5 # performs number of steps for quantum walk
for i in range(n):
    qWalk.append(step_gate,[0,1,2,3,4,5,6,7]) #append the step gate for number of desired walks 

#measure the p-qubits only 
qWalk.measure(1,0)
qWalk.measure(3,1)
qWalk.measure(5,2)
qWalk.measure(7,3)
    
#QRW is now ready    
qWalk.draw()

#make the measurement of the above circuit
simulator = Aer.get_backend('aer_simulator')
result = execute(qWalk, backend=simulator, shots=2048).result()
bit_count = result.get_counts()

print("binary counts", bit_count) #print the counts

bit_list = list(bit_count.keys()) # get the keys of the binary dictionary
count_list= list(bit_count.values()) # get the values of the binary dictionary
#print(count_list)

decimal_list = [] #make an empty decimal list to store the values of decimal count

for binary_string in bit_list:
    decimal = int(binary_string, 2)  # Convert binary string to decimal
    decimal_list.append(decimal)    # insert the decimal values to decimal_list

print("Decimal list:", decimal_list)
dec_string_list = [str(decimal) for decimal in decimal_list]
#print(dec_string_list)

dec_count = dict(zip(dec_string_list,count_list)) #make the dictionary for decimal count
print("decimal counts", dec_count)

#plot the measurement 
plot_histogram(dec_count, color='midnightblue', title='plot for QRW circuit for shots =2048', figsize = (12,8))

#transpiler to decompose the circuit
basis = ['h','cx','rz'] # use the basis gates as given
result = transpile(step_op,basis_gates = basis)
print(result) #transpiled circuit

# define a particular random walk
def randomwalk1D(n):
    x, y = 0, 3 #start with the initial state
    # Generate the time points [1, 2, 3, ... , n]
    timepoints = np.arange(n + 1)
    positions = [y]
    directions = ["UP", "DOWN"]
    for i in range(1, n + 1):
        # Randomly select either UP or DOWN
        step = random.choice(directions)
        # Move the object up or down
        if step == "UP":
            y += 1
        elif step == "DOWN":
            y -= 1
        # Keep track of the positions
        positions.append(y)
        final = y
    return  final


 
n = 1000 #choose repititions
positions = np.zeros(n) #create a position space for storing our walk positions
walkstep = 5 #choose the number of random walk steps
for i in range(n):
    positions[i] = randomwalk1D(walkstep)
#print(positions)

# Plotting
unique, counts = np.unique(positions, return_counts=True)  # generates - bit-string, counts
unique[0]= 14

#convert the integer to string format for plotting later 
points = list(map(str, unique.astype(int)))
#print(points)

#lists the counts of corresponsing bit strings
plot = dict(zip(points, counts))

print(plot)

#plot_histogram(plot)
legend = ['QRW', 'Classical']
plot_histogram([dec_count, plot], legend=legend, color=['crimson','midnightblue'],
                title="QRW vs Classical random walk comparison, shots = 1000", figsize = (12,8))




oracle = QuantumCircuit(4)
oracle.cx(0,3)
oracle.cx(2,3)
oracle.draw()

bv = QuantumCircuit(4,3) #make the quantum Circuit

#input the Hadamards
bv.x(3)
for i in range(4):
    bv.h(i)
bv.barrier()

#input the oracle from the avove cell
bv.cx(0,3)
bv.cx(2,3)
bv.barrier()

#complete it with Hadamard
for i in range(3):
    bv.h(i)
bv.barrier()

#make the measurement
for i in range(3):
    bv.measure(i,i)

bv.draw()

#for plotting: choose a required simulator
simulator = Aer.get_backend("qasm_simulator")
result = execute(bv, backend = simulator, shots = 1000).result() #execute the circuit to get measurements
counts = result.get_counts() #get the counts for each measurement
print(counts)
plot_histogram(counts,  color='midnightblue', title='B-V Algorithm for secret string a ',figsize = (12,8)) #plot the histogram

def decimalToBinary(n):
    ''' converts a decimal interger to binary '''
    return "{0:b}".format(int(n))
#ask to choose a decimal input
a_deci = int(input('Choose a number between 0 to 7 :')) 

# 3bit binary string equivalent
a = decimalToBinary(a_deci)  
print(a) #converts the decimal string to binary bits


#make the B-V circuit 
bv_circ = QuantumCircuit(len(a)+1,len(a))

#first insert all the hadamards
bv_circ.h(range(len(a)))
bv_circ.x(len(a))
bv_circ.h(len(a))

bv_circ.barrier()

#make the oracle
for i, query in enumerate(reversed(a)):  #cause qiskit uses a reverse ordering LSB-MSB
    if query == "1":
        bv_circ.cx(i, len(a))
              
bv_circ.barrier()

#again insert the hadamards
bv_circ.h(range(len(a)))

bv_circ.barrier()

#measure the circuit
bv_circ.measure(range(len(a)),range(len(a)))

bv_circ.draw("mpl")


#follow rthe similar procedure from the above cells for measurement plotting
simulator = Aer.get_backend("qasm_simulator")
result = execute(bv_circ, backend = simulator, shots = 1000).result() #run the circuit 
counts = result.get_counts() # get the counts
print(counts)
plot_histogram(counts,  color='midnightblue', title='B-V Algorithm for a chosen string  ',figsize = (12,8)) #plot Histogram

circuit = QuantumCircuit(4)
circuit.cx(1,2)
circuit.cx(0,1)
circuit.h(2)
circuit.cx(0,3)

circuit.measure_all()
circuit.draw()


#make the measurement of the above circuit
simulator = Aer.get_backend('qasm_simulator')
result = execute(circuit, backend=simulator, shots=1000).result() #run the circuit 
counts_old = result.get_counts() # get the counts
from qiskit.tools.visualization import plot_histogram
plot_histogram(counts_old, color='midnightblue', title='Measurement plot for circuit 2.4a) ',figsize = (12,8))

#transpiler to decompose the circuit
result = transpile(circuit, basis_gates=['h', 'cz'], optimization_level=3) 
result.draw(output='mpl') #draw the circuit

#making the transpiled Circuit
#insert all the gates with reference to the transpiled circuit and build it.
circuit1 = QuantumCircuit(4)
circuit1.h(2)
circuit1.h(3)
circuit1.cz(1,2)
circuit1.h(1)
circuit1.cz(0,1)
circuit1.h(1)
circuit1.cz(0,3)
circuit1.h(3)

print('the transpiled circuit \n')
circuit1.measure_all()
circuit1.draw()


#make the measurement of the transpiled circuit
simulator = Aer.get_backend('qasm_simulator')
result = execute(circuit1, backend=simulator, shots=1000).result()
counts_new = result.get_counts()
legend = ['old', 'new (transpiled)']

plot_histogram([counts_old, counts_new], legend=legend, color=['crimson','midnightblue'],
                title="Measurement comparison for old vs transpiled circuit, shots =1000", figsize = (12,8))

basis_gates = ["cx", "id", "rz", "sx", "x"]
adjacency_matrix = [[0,1],[1,0],[1,2],[2,1],[1,3],[3,1],[3,5],[5,3],[4,5],[5,4],[5,6],[6,5]]
result = transpile(circuit, basis_gates = basis_gates, coupling_map = adjacency_matrix)
result.draw('mpl')

from qiskit.providers.fake_provider import FakeMontrealV2 #import the fakebackends to get the topology of 'montreal'
backend = FakeMontrealV2()
new_circuit = transpile(circuit, backend=backend) #transpile
new_circuit.draw()

