# import necessary libraries
import numpy as np
from pprint import pprint
from qiskit import QuantumProgram
from qiskit.tools.visualization import plot_histogram
#import Qconfig
# When working worth external backends (more on this below),
# be sure that the working directory has a 
# Qconfig.py file for importing your APIToken from 
# your IBM Q Experience account. 
# An example file has been provided, so for working
# in this notebook you can simply set 
# the variable values to your credentials and rename 
# this file as 'Qconfig.py'

# This initial state register
# can be realized in python by creating an instance of the 
# QISKit QuantumProgram Class with a quantum register of 2 qubits 
# and 2 classical ancilla bits for measuring the states
i = QuantumProgram()

n = 2
i_q = i.create_quantum_register("i_q", n)
i_c = i.create_classical_register("i_c", n)

#i.set_api(Qconfig.APItoken, Qconfig.config['url']) # set the APIToken and API url

i.available_backends() #check backends - if you've set up your APIToken properly you
#should be able to see the quantum chips and simulators at IBM

for backend in i.available_backends(): #check backend status
    print(backend)
    pprint(i.get_backend_status(backend))

def execute_and_plot(qp, circuits, backend = "local_qasm_simulator"):
    """Executes circuits and plots the final 
    state histograms the for each circuit.
    Adapted from 'execute_and_plot' function
    in the beginners_guide_composer_examples
    notebook provided in IBM's QISKit
    tutorial library on GitHub.
    
    Args:
        qp: QuantumProgram containing the circuits
        circuits (list): list of circuits to execute
        backend (string): allows for specifying the backend 
        to execute on. Defaults to local qasm simulator
        downloaded with QISKit library, but can be specified
        to run on an actual quantum chip by using the string
        names of the available backends at IBM.
    """
    # Store the results of the circuit implementation
    # using the .execute() method
    results = qp.execute(circuits, backend = backend)
 
    for circuit in circuits:
        plot_histogram(results.get_counts(circuit)) # .get_counts()
        # method returns a dictionary that maps each possible
        # final state to the number of instances of 
        # said state over n evaluations 
        # (n defaults to 1024 for local qasm simulator),
        # where multiple evaluations are a necessity since
        # quantum computation outputs are statistically
        # informed

# Initialize circuit:
cnot_i_00 = i.create_circuit("cnot_i_00", [i_q], [i_c]) 
# Note: qubits are assumed by QISKit
# to be initialized in the |0> state

# Apply gates according to diagram:
cnot_i_00.cx(i_q[0], i_q[1]) # Apply CNOT on line 2 controlled by line 1

# Measure final state:
cnot_i_00.measure(i_q[0], i_c[0]) # Write qubit 1 state onto classical ancilla bit 1
cnot_i_00.measure(i_q[1], i_c[1]) # Write qubit 2 state onto classical ancilla bit 2

# Display final state probabilities:
execute_and_plot(i, ["cnot_i_00"])

print(i.get_qasm('cnot_i_00'))

# Initialize circuit:
cnot_i_01 = i.create_circuit("cnot_i_01", [i_q], [i_c])
cnot_i_01.x(i_q[0]) # Set the 1st qubit to |1> by flipping 
# the initialized |0> with an X gate before implementing
# the circuit 

# Apply gates according to diagram:
cnot_i_01.cx(i_q[0], i_q[1]) # Apply CNOT controlled by line 1

# Measure final state:
cnot_i_01.measure(i_q[0], i_c[0])
cnot_i_01.measure(i_q[1], i_c[1])

# Display final state probabilities:
execute_and_plot(i, ["cnot_i_01"])

# Initialize circuit:
cnot_i_10 = i.create_circuit("cnot_i_10", [i_q], [i_c])
cnot_i_10.x(i_q[1]) # Set the 2nd qubit to |1>

# Apply gates according to diagram:
cnot_i_10.cx(i_q[0], i_q[1]) # Apply CNOT controlled by line 1

# Measure final state:
cnot_i_10.measure(i_q[0], i_c[0])
cnot_i_10.measure(i_q[1], i_c[1])

# Display final state probabilities:
execute_and_plot(i, ["cnot_i_10"])

# Initialize circuit:
cnot_i_11 = i.create_circuit("cnot_i_11", [i_q], [i_c])
cnot_i_11.x(i_q[0]) # Set the 1st qubit to |1>
cnot_i_11.x(i_q[1]) # Set the 2nd qubit to |1>

# Apply gates according to diagram:
cnot_i_11.cx(i_q[0], i_q[1]) # Apply CNOT controlled by line 1

# Measure final states:
cnot_i_11.measure(i_q[0], i_c[0])
cnot_i_11.measure(i_q[1], i_c[1])

# Display final state probabilities:
execute_and_plot(i, ["cnot_i_11"])

# For circuit ii, we can again create a QuantumProgram instance to 
# realize a quantum register of size 2 with 2 classical ancilla bits
# for measurement
ii = QuantumProgram()

n = 2
ii_q = ii.create_quantum_register("ii_q", n)
ii_c = ii.create_classical_register("ii_c", n)

#ii.set_api(Qconfig.APItoken, Qconfig.config['url']) # set the APIToken and API url   

ii.available_backends() #check backends - if you've set up your APIToken properly you
#should be able to see the quantum chips and simulators at IBM

for backend in ii.available_backends(): #check backend status
    print(backend)
    pprint(ii.get_backend_status(backend))

# Initialize circuit:
cnot_ii_00 = ii.create_circuit("cnot_ii_00", [ii_q], [ii_c])

# Apply gates according to diagram:
cnot_ii_00.h(ii_q) # Apply hadamards in parallel, note that specifying
# a register a a gate method argument applies the operation to all
# qubits in the register
cnot_ii_00.cx(ii_q[1], ii_q[0]) #apply CNOT controlled by line 2
cnot_ii_00.h(ii_q) # Apply hadamards in parallel

# Measure final state:
cnot_ii_00.measure(ii_q[0], ii_c[0])
cnot_ii_00.measure(ii_q[1], ii_c[1])

# Display final state probabilities
execute_and_plot(ii, ["cnot_ii_00"])

# Initialize circuit:
cnot_ii_01 = ii.create_circuit("cnot_ii_01", [ii_q], [ii_c])
cnot_ii_01.x(ii_q[0]) # Set the 1st qubit to |1>

# Apply gates according to diagram:
cnot_ii_00.h(ii_q) # Apply hadamards in parallel
cnot_ii_01.cx(ii_q[1], ii_q[0]) # Apply CNOT controlled by line 2
cnot_ii_00.h(ii_q) # Apply hadamards in parallel

# Measure final state:
cnot_ii_01.measure(ii_q[0], ii_c[0])
cnot_ii_01.measure(ii_q[1], ii_c[1])

# Display final state probabilities:
execute_and_plot(ii, ["cnot_ii_01"])

# Initialize circuits
cnot_ii_10 = ii.create_circuit("cnot_ii_10", [ii_q], [ii_c])
cnot_ii_10.x(ii_q[1]) # Set the 2nd qubit to |1>

# Apply gates according to diagram:
cnot_ii_00.h(ii_q) # Apply hadamards in parallel
cnot_ii_10.cx(ii_q[1], ii_q[0]) # Apply CNOT controlled by line 2
cnot_ii_00.h(ii_q) # Apply hadamards in parallel

# Measure final state:
cnot_ii_10.measure(ii_q[0], ii_c[0])
cnot_ii_10.measure(ii_q[1], ii_c[1])

# Display final state probabilities:
execute_and_plot(ii, ["cnot_ii_10"])

# Initialize circuits:
cnot_ii_11 = ii.create_circuit("cnot_ii_11", [ii_q], [ii_c])
cnot_ii_11.x(ii_q[0]) # Set the 1st qubit to |1>
cnot_ii_11.x(ii_q[1]) # Set the 2nd qubit to |1>

# Apply gates according to diagram:
cnot_ii_00.h(ii_q) # Apply hadamards in parallel
cnot_ii_11.cx(ii_q[1], ii_q[0]) # Apply CNOT controlled by line 2
cnot_ii_00.h(ii_q) # Apply hadamards in parallel

# Measure final state
cnot_ii_11.measure(ii_q[0], ii_c[0])
cnot_ii_11.measure(ii_q[1], ii_c[1])

# Display final state probabilities
execute_and_plot(ii, ["cnot_ii_11"])

def circuit_i():
    i = QuantumProgram()
    i_q = i.create_quantum_register('i_q', 2)
    i_c = i.create_classical_register('i_c', 2)
    initial_states = ['00','01','10','11']
    initial_circuits = {state: i.create_circuit('%s'%(state), [i_q], [i_c]) \
                       for state in initial_states}
    final_circuits = {}
    for state in initial_states:
        if state[0] is '1':
            initial_circuits[state].x(i_q[0])
        if state[1] is '1':
            initial_circuits[state].x(i_q[1])
            
        initial_circuits[state].cx(i_q[0], i_q[1])
        
        initial_circuits[state].measure(i_q[0], i_c[0])
        initial_circuits[state].measure(i_q[1], i_c[1])
        final_circuits[state] = initial_circuits[state]
    return i

def circuit_ii():
    ii = QuantumProgram()
    ii_q = ii.create_quantum_register('ii_q', 2)
    ii_c = ii.create_classical_register('ii_c', 2)
    initial_states = ['00','01','10','11']
    circuits = {state: ii.create_circuit('%s'%(state), [ii_q], [ii_c]) \
                       for state in initial_states}
    for state in initial_states:
        if state[0] is '1':
            circuits[state].x(ii_q[0])
        if state[1] is '1':
            circuits[state].x(ii_q[1])
            
        circuits[state].h(ii_q)
        circuits[state].cx(ii_q[1], ii_q[0])
        circuits[state].h(ii_q)
        
        circuits[state].measure(ii_q[0], ii_c[0])
        circuits[state].measure(ii_q[1], ii_c[1])
    return ii

i = circuit_i()
ii = circuit_ii()
#i.set_api(Qconfig.APItoken, Qconfig.config['url'])   
#ii.set_api(Qconfig.APItoken, Qconfig.config['url'])   

results_i = i.execute(list(i.get_circuit_names()))
results_ii = ii.execute(list(ii.get_circuit_names()))

results_i_mapping = {circuit: results_i.get_counts(circuit) for circuit in list(i.get_circuit_names())}
results_ii_mapping = {circuit: results_ii.get_counts(circuit) for circuit in list(ii.get_circuit_names())}

print(results_i_mapping)
print(results_ii_mapping)

