%matplotlib inline
import matplotlib.pyplot as plt

import projectq.setups.ibm
from projectq.backends import IBMBackend
from projectq.ops import Measure, Entangle, All
from projectq import MainEngine

"""
Explanation: Running ProjectQ code on IBM Q devices
In this tutorial, we will see how to run code on IBM Q devices directly from within ProjectQ. All that is needed is an IBM Q Experience user account. To sign up, visit https://quantumexperience.ng.bluemix.net/.
ProjectQ supports two IBM Q devices called ibmqx4 and ibmqx5 which feature 5 and 16 qubits, respectively. Let us start with entangling the qubits of the 5-qubit device:
Entangling 5 qubits
First, we import all necessary operations (Entangle, measurement), the back-end (IBMBackend), and the main compiler engine (MainEngine). The Entangle operation is defined as a Hadamard gate on the first qubit (creates an equal superposition of |0> and |1>), followed by controlled NOT gates acting on all other qubits controlled on the first.
End of explanation
"""


eng = MainEngine(IBMBackend(use_hardware=True, num_runs=1024,
                            verbose=False, device='ibmqx4'),
                 engine_list=projectq.setups.ibm.get_engine_list())

"""
Explanation: Next, we instantiate a main compiler engine using the IBM Q back-end and the predefined compiler engines which take care of the qubit placement, translation of operations, etc.:
End of explanation
"""


def run_entangle(eng, num_qubits):
    # allocate a quantum register of 5 qubits
    qureg = eng.allocate_qureg(num_qubits)

    # entangle the qureg
    Entangle | qureg

    # measure; should be all-0 or all-1
    All(Measure) | qureg

    # run the circuit
    eng.flush()

    # access the probabilities via the back-end:
    # results = eng.backend.get_probabilities(qureg)
    # for state in results:
    #     print("Measured {} with p = {}.".format(state, results[state]))
    # or plot them directly:
    histogram(eng.backend, qureg)
    plt.show()

    # return one (random) measurement outcome.
    return [int(q) for q in qureg]

run_entangle(eng, num_qubits=5)  # run it

"""
Explanation: If use_hardware is set to False, it will use the IBM Q simulator instead. num_runs specifies the number of samples to collect for statistics, verbose=True would output additional information which may be helpful for debugging, and the device parameter lets users choose between the two devices ("ibmqx4" and "ibmqx5").
With our compiler set up, we can now allocate our qubits, entangle them, measure the outcome, and then flush the entire circuit down the compilation pipeline such that it is executed (and measurements are registered). Note that there are many jobs queued for execution on the IBM Q device and, as a result, our execution times out. We will learn how to retrieve our results despite this time out.
End of explanation
"""


eng = MainEngine(IBMBackend(use_hardware=True, num_runs=1024,
                            verbose=False, device='ibmqx4',
                            retrieve_execution="5b557df2306393003b746da2"),  # provide job ID
                 engine_list=projectq.setups.ibm.get_engine_list())

run_entangle(eng, num_qubits=5)

"""
Explanation: Retrieving a timed-out execution
Sometimes, the queue is very long and the waiting times may exceed the limit of 5 minutes. In this case, ProjectQ will raise an exception which contains the job ID, as could be seen above, where the job ID was 5b557df2306393003b746da2.
In order to still retrieve all results at a later point in time, one can simply re-run the entire program using a slightly modified back-end:
End of explanation
"""


import projectq.setups.ibm16  # import setup which contains the grid mapper
eng = MainEngine(IBMBackend(use_hardware=True, num_runs=1024,
                            verbose=False, device='ibmqx5'),  # use ibmqx5 now
                 engine_list=projectq.setups.ibm16.get_engine_list())  # note: ibm16 setup

"""
Explanation: Entangling more qubits: Using ibmqx5
If you have access to the 16-qubit device as well, you can also use ProjectQ to run your quantum programs on this device. ProjectQ contains a 2D grid mapper, which takes care of the mapping for you. We only have to change two things in order to use the 16-qubit chip as opposed to the 5-qubit chip:
1) Import the new 16-qubit setup which contains the compiler engines for this device
2) Modify the device parameter in the IBMBackend to "ibmqx5"
Therefore, in order to entangle more than 5 qubits, we can simply write
End of explanation
"""


run_entangle(eng, num_qubits=8)

"""
Explanation: and then re-run the example from before via run_entangle(eng, num_qubits). If an execution times out, it can also be retrieved at a later point by providing the additional retrieve_execution="execution_id" parameter to the IBMBackend (but this time with device='ibmqx5').
End of explanation
"""


