#initialization 
import matplotlib.pyplot as plt
import numpy as np

#ignore deprecation warnings because they are annoying (not recommened generally)
from warnings import simplefilter 
simplefilter(action='ignore', category=DeprecationWarning)

# Importing standard Qiskit libraries
from qiskit import *
from qiskit.tools.jupyter import *
from qiskit.visualization import *
from qiskit.quantum_info import *
from qiskit.circuit.library import *
from ibm_quantum_widgets import *

NUM_QUBITS = 2
# let's construct a multi-qubit quantum register
qr = QuantumRegister(NUM_QUBITS, 'q')

qc = QuantumCircuit(qr, name='my-circuit')
# let's create a Bell's state
qc.h(0)
qc.cx(0,1)
qc.measure_all()
qc.draw()

# let's construct a multi-bit classical register
cr = ClassicalRegister(NUM_QUBITS, 'c')
qc = QuantumCircuit(qr, cr, name='my-circuit')
# explicitly measure qubits [0,1] into classical bits [0,1]
qc.measure(qr, cr)
# alternatively: qc.measure([0,1], [0,1])
qc.draw()

# Bell state 0
bell_0 = QuantumCircuit(2)
bell_0.h(0)
bell_0.cx(0,1)

sv = Statevector.from_label('00')
#evolve the initial state through the circuit
sv_bell1 = sv.evolve(bell_0) 
sv_bell1.draw('latex')  

# Bell state 1
bell_1 = QuantumCircuit(2)
bell_1.x(0)
bell_1.h(0)
bell_1.cx(0,1)

sv = Statevector.from_label('00')
#evolve the initial state through the circuit
sv_bell2 = sv.evolve(bell_1) 
sv_bell2.draw('latex')

# Bell state 2
bell_2 = QuantumCircuit(2)
bell_2.x(0)
bell_2.h(1)
bell_2.cx(1,0)

sv = Statevector.from_label('00')
#evolve the initial state through the circuit
sv_bell3 = sv.evolve(bell_2) 
sv_bell3.draw('latex')

# create the last remaining Bell state on your own
bell_3 = QuantumCircuit(2)

### ADD CODE BELOW

### DON'T ADD CODE BELOW

sv = Statevector.from_label('00')
#evolve the initial state through the circuit
sv_bell3 = sv.evolve(bell_3)
sv_bell3.draw('latex')

# state of equal superposition of basis states
max_sp = QuantumCircuit(2)
max_sp.h(0)
max_sp.h(1)

sv = Statevector.from_label('00')
# evolve the initial state through the circuit

### ADD CODE BELOW

### DON'T ADD CODE BELOW

qc = QuantumCircuit(1)
qc.h(0)
qc.h(0)
qc.draw('mpl')

backend = BasicAer.get_backend('qasm_simulator')
qc = QuantumCircuit(1)
qc.h(0)
qc.h(0)
transpile(qc, backend=backend).draw('mpl')

backend = BasicAer.get_backend('qasm_simulator')
qc = QuantumCircuit(1)
qc.h(0)
qc.barrier()
qc.h(0)
transpile(qc, backend=backend).draw('mpl')

# try it out yourself with different gates: instead of Hadamard, try Pauili gates
my_qc = QuantumCircuit(1)

### ADD CODE BELOW

### DON'T ADD CODE BELOW

transpile(my_qc, backend=backend).draw('mpl')
# did you get what you expected?

qc = QuantumCircuit(1)
qc.x(0)
qc.depth()

qc = QuantumCircuit(2)
qc.x([0,1])
qc.x(1)
qc.depth()
# draw the circuit yourself to see why the depth has increased now

### ADD CODE BELOW

### DON'T ADD CODE BELOW

qc = QuantumCircuit(2)
qc.x(0)
qc.barrier(0)
qc.h(0)
qc.cx(0,1)
qc.depth()
# draw the circuit yourself to see why the depth has increased now
# hint: the longest path is not always the lenth of the longest sequence along one channel
qc.draw('mpl')
# the second qubit only has a CX gate applied to it, but has to "wait" for the first qubit to provide the control
# also, barier doesn't count

# in the following we will use the implementation of Grover's algorithm in Qiskit
from qiskit.algorithms import Grover, AmplificationProblem

# let's construct an oracle with a single solution
sv = Statevector.from_label('010')
problem = AmplificationProblem(oracle=sv)

grover = Grover(iterations=2).construct_circuit(problem=problem)

# the original circuit contains unitaries
# transpile the circuit in terms of basis gates to see what it is "made of"
t_grover = transpile(grover, basis_gates=['cx', 'u3'])
t_grover.draw('mpl', fold=-1)

# draw the original cicuit yourself
grover.draw('mpl', fold=-1)

# lets's try to evolve the initial state through the circuit like we have done for Bell states
sv = Statevector.from_label('000')
sv_ev = sv.evolve(grover)
sv_ev.draw('latex')
# it is obvious from statevector evolutions, that the state 010
# has the best chances to be measured, as desired

# now let's obtain the same result from the QASM simulation
# for that we need to measure the qubits in the end of our circuit
grover.measure_all()

backend= BasicAer.get_backend('qasm_simulator')
job = execute(grover, backend, shots=1024)
result = job.result()
plot_histogram(result.get_counts())

# finally let's see how our circuit performs on actual hardware
# you will need your IBM Quantum account
IBMQ.load_account()
provider = IBMQ.get_provider(hub='ibm-q')

# sometimes you might need to wait when running on real devices
# let's monitor the status of our job
from qiskit.tools import job_monitor

# to call a real backend let's pick the least busy one, which satisfies out requirments
from qiskit.providers.ibmq import least_busy

suitable_devices = provider.backends(filters = lambda b: b.configuration().n_qubits >= 3
                                                        and not b.configuration().simulator
                                                        and b.status().operational==True)

backend = least_busy(backends=suitable_devices)
print("We are runnig our circuit on ", backend)

job = execute(grover, backend=backend)
job_monitor(job)

# we see that results from a real device also match our expectations
# the algorithm successfully finds the marked string
result = job.result()
counts = result.get_counts()

# plot the histogramm yourself
### ADD CODE BELOW

### DON'T ADD CODE BELOW

# let's again use our transpiled Grover circuit to demonstarte the idea

# Notice: QASM doesn't look "nice". This is a low-level language which can be understood by quantum hardware.
# If you want to take a look at the QASM code make sure to specify 'formatted=True' which makes the code readable by a human being.
t_grover.qasm(formatted=True)

# store the circuit to a file in the follwing way
qasm_code = t_grover.qasm(filename='grover.qasm')

# to get the circuit back from QASM file execute the following
my_qc = QuantumCircuit.from_qasm_file('grover.qasm')
my_qc.draw('mpl', fold=-1)

# let's create an arbitrary Operator from a matrix
A = np.array([[0,1,0], [1,0,0], [0,1,1]])
op = Operator(A)
# Operator contains information about its matrix representation and input and output dimensions
op

# create an Operator from a circuit yourself
qc = QuantumCircuit(2)
qc.h([0,1])
qc.cx(0,1)

op = Operator(qc)
op

A = Operator(Pauli(label='X'))
B = Operator(Pauli(label='Z'))
A.expand(B) # B x A 

A = Operator(Pauli(label='X'))
B = Operator(Pauli(label='Z'))
A.tensor(B) # result A x B

III = Operator(np.eye(2 ** 3))
XX = Operator(Pauli(label='XX'))
XIX = III.compose(XX, qargs=[0,2])
XIX # the resulting operator is X x I x X

# compose an XIXZ Operator from IIII and XX and Z Operators

### ADD CODE BELOW

### DON'T ADD CODE BELOW

# with Qiskit you can find the state fidelity between two quantum states

# let's investigate Bell states
state_fidelity(sv_bell3, sv_bell1)

basis_state_00 = Statevector.from_label('00')
state_fidelity(basis_state_00, sv_bell1)

basis_state_00 = Statevector.from_label('00')
state_fidelity(basis_state_00, sv_bell3)

# let's find process fidelity of X and H Operators
op_1 = Operator([[0,1],[0,1]])
op_2 = Operator([[1,1],[1,-1]])/np.sqrt(2)

F = process_fidelity(op_1, op_2)
F

# what about fidelity of X Operator with itself multiplied with a complex phase
op_1 = Operator([[0,1],[0,1]])
op_2 = op_1*np.exp(0.5j)
# calculate the process fidelity yourself

### ADD CODE BELOW

### DON'T ADD CODE BELOW

# as you expected, the fidelity is independent of global phase

# encountered a compatibility error? Check which version of Qiskit you are using
import qiskit
qiskit.__qiskit_version__

# display the entire version table
%qiskit_version_table

# Let's have an overview information on all the IBMQ backends that are available
%qiskit_backend_overview

# now let's pick one device and investigate it 
from qiskit.visualization import plot_gate_map
backend = provider.get_backend('ibmq_bogota')

plot_gate_map(backend, plot_directed=True)

plot_error_map(backend)

# let's plot the layout of our Bell state circuit transpiled for our target backend
transpiled_bell = transpile(bell_0, backend=backend)
plot_circuit_layout(transpiled_bell, backend=backend)

# display the layout of transpiled Grover's circuit on our target backend
transpiled_grover = transpile(grover, backend=backend)
plot_circuit_layout(transpiled_grover, backend=backend)

# what if we want to specify a custom mapping of our circuit qubits to device qubits?
# let's create an initial layout
initial_layout = [2,3,4] # virtual to physical: virtual qubits are ordered
transpiled_qc = transpile(grover, backend=backend, initial_layout=initial_layout)
plot_circuit_layout(transpiled_qc, backend=backend)

# for example, let's draw our Bell's state circuit in text mode
bell_0.draw('text')

# qubits in reversed order
bell_0.draw('mpl', reverse_bits=True)

# More customization
style = {'linecolor': 'red', 'backgroundcolor': 'grey'}
bell_0.draw('mpl',scale = 1.2, style=style, fold=-1)

plot_bloch_vector([0,1,0])

plot_state_city(bell_0)
# to understand why the 'city' looks like this consider the density matrix of this state

plot_state_hinton(bell_0)

plot_state_qsphere(bell_0)

plot_state_paulivec(bell_0)

plot_bloch_multivector(bell_0)
# here, the bloch spheres represent maximally entangled qubits as zero-length vectors. Hence, no arrows.

# a more informative example
psi = Statevector.from_label('+1')
plot_bloch_multivector(psi)
# the first qubit is in state |1>
# the second qubit is in the superposition of |0> and |1>

rho = DensityMatrix.from_instruction(bell_0)
rho.draw('latex', prefix='\\rho_{Bell_0} = ')

plot_state_city(rho.data, title='Density Matrix')

# prepare a density matrix for the state of the equal superposition of 2 basis states
my_qc = QuantumCircuit(2)
my_qc.h([0,1])
my_rho = DensityMatrix.from_instruction(my_qc)
my_rho.draw('latex', prefix='\\rho_{equal} = ')

# prepare a density matrix for a mixed one-qubit state in an equal mixture of |0> and |1>

### ADD CODE BELOW

### DON'T ADD CODE BELOW

qr = QuantumRegister(3, name='q')
qc = QuantumCircuit(qr)
qc.ccx(control_qubit1=qr[1], control_qubit2=qr[2], target_qubit=qr[0])
qc.draw('mpl')

# Toffoli gate has the following matrix representation for the above configuration
matrix = Operator(qc).data
array_to_latex(matrix, prefix="CCX = ")

qr = QuantumRegister(2, name='q')
qc = QuantumCircuit(qr)
qc.swap(qubit1=qr[0], qubit2=[1])
qc.draw('mpl')

# SWAP gate has the following matrix representation for the above configuration
matrix = Operator(qc).data

# display the matrix in latex
array_to_latex(matrix, prefix="SWAP = ")