#!pip install qiskit

#import qiskit.tools.jupyter
#%qiskit_version_table
#%qiskit_copyright

#pip install qcircuit

#pip install pdflatex

from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, execute
import numpy as np
import qiskit.quantum_info as qi
import qiskit
from qiskit import Aer
from qiskit.tools.jupyter import *
from qiskit.visualization import *
from qiskit.visualization.circuit import *
from qiskit import transpile, assemble
import matplotlib.pyplot as plt
from qiskit.circuit import Parameter

# Supress warnings
import warnings
warnings.filterwarnings("ignore")

# Create a quantum circuit instance with one qubit
circuit = QuantumCircuit(2,0)

# Apply Hadamard Gate
circuit.h(0)

# Draw quantum circuit
circuit.draw(output = "mpl")

# Print all the available quantum simulators
Aer.backends()

# Load statevector simulator
simulator = Aer.get_backend("statevector_simulator")

# Create quatum job to execute
job = simulator.run(circuit)

# Execute the job
result = job.result()

state_ex=result.get_statevector(circuit) #if possible mantains phases as fractions
state = result.get_statevector(circuit, decimals = 4) #using parameter decimals, approximates the fractions

state_ex.draw(output='latex')

state.draw(output='latex')

state.draw(output = "bloch")

#to do statevector simulation more easily
def statevect_sim(circuit: QuantumCircuit):
    simulator=Aer.get_backend("statevector_simulator")
    state=simulator.run(circuit).result().get_statevector(circuit)    
    print(state)

    return state.draw('latex')

# Apply RX / RY / S /T gates
circuit2=QuantumCircuit(1,0)
circuit2.rx(np.pi/2,0) #rx(theta, n) gate is a single-qubit gate rotation of angle theta around the x-axis of the qubit n
circuit2.draw(output='mpl') 

statevect_sim(circuit2)

simulator.run(circuit2).result().get_statevector(circuit2).draw(output = "bloch")

circuit3=QuantumCircuit(1,0)
circuit3.ry(np.pi*3/2,0) #ry(theta, n) gate is a single-qubit gate rotation of angle theta around the y-axis of the qubit n
circuit3.draw(output='mpl')

#create the job
job3=simulator.run(circuit3)
#execute the job
result3=job3.result()
#create the state
state3_ex=result3.get_statevector(circuit3)
state3=result3.get_statevector(circuit3,decimals=4)


state3_ex.draw(output='latex')

state3.draw(output='latex')

#or we can just use
statevect_sim(circuit3)

state3.draw(output='bloch')

circuit4=QuantumCircuit(1,0)
circuit4.rx(np.pi/4,0)
circuit4.s(0) #s(n) gate is a single-qubit gate rotation of angle pi/2 around the z-axis of the qubit n
circuit4.draw(output='mpl')

statevect_sim(circuit4)

simulator.run(circuit4).result().get_statevector(circuit4).draw(output = "bloch")

circuit5=QuantumCircuit(1,0)
circuit5.h(0) #the state is in |+>
circuit5.t(0) #t(n) gate is a single-qubit gate rotation of angle pi/4 around the z-axis of the qubit n
circuit5.draw(output='mpl')

statevect_sim(circuit5)

simulator.run(circuit5).result().get_statevector(circuit5).draw(output = "bloch")

# Create quantum circuit with two quantum registers and two classical registers
circuit_bell = QuantumCircuit(2, 2)

# Create Bell state following the expression above
circuit_bell.h(0)
circuit_bell.cx(0,1)

statevect_sim(circuit_bell)

# Visualize the circuit
circuit_bell.draw(output = 'mpl')

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

statevect_sim(circuit)

circuit.draw('mpl')

circuit=QuantumCircuit(2)
circuit.x(0)
circuit.h(0)
circuit.x(1)
circuit.cx(0,1)
circuit.draw('mpl')

circuit.swap(0,1) #qiskit visualization of qubits is reversed

statevect_sim(circuit)

#from qiskit.visualization import plot_state_city, plot_state_hinton, plot_state_qsphere, plot_histogram

state2=simulator.run(circuit2).result().get_statevector(circuit2)
state2.draw(output='latex')

plot_state_city(state2) #Plot two 3d bar graphs (two dimensional) of the real and imaginary part of the density matrix rho

plot_state_hinton(state2)

#seaborn package required
plot_state_qsphere(state2)

state=simulator.run(circuit).result().get_statevector(circuit)
state.draw('latex')

plot_state_city(state)

plot_state_hinton(state)

plot_state_qsphere(state)

state_bell=simulator.run(circuit_bell).result().get_statevector(circuit_bell)
state_bell.draw('latex')

plot_state_city(state_bell)

plot_state_hinton(state_bell)

plot_state_qsphere(state_bell)

# Run the quantum circuit on a unitary simulator backend
backend = Aer.get_backend('unitary_simulator') #Unitary Simulator is a simulator provided by Qiskit that calculates and returns the unitary matrix that represents the Quantum Circuit

# Create job and execute
job = backend.run(circuit_bell)
result = job.result()

# Show the results
print(result.get_unitary(circuit_bell, decimals=3))


# Initialize quantum and classical registers
qr = QuantumRegister(2)
cr = ClassicalRegister(2) #classical registers are needed to store the results of measurements (if we use the command measure(qr,cr))

# Build quantum circuit
circuit_qasm = QuantumCircuit(qr, cr)

#Create Bell state
circuit_qasm.h(0)
circuit_qasm.cx(0,1)

# Measure qubits
circuit_qasm.measure(qr, cr) #circuit_qasm.measure_all() memeasures all qubits, creates a new register to store the measurements and adds a barrier before the measurements

circuit_qasm.draw(output = 'mpl')

backend = Aer.get_backend("qasm_simulator")
shots = 1024 #1024 is the standard number of shots
job = execute(circuit_qasm, backend, shots = shots)
result = job.result()

counts = result.get_counts()
plot_histogram(counts)

qr=QuantumRegister(2)
cr=ClassicalRegister(2)
circ_bell=QuantumCircuit(qr,cr)
circ_bell.x(0)
circ_bell.h(0)
circ_bell.cx(0,1)

circ_bell.measure(qr,cr)

circ_bell.draw('mpl')

job_bell=execute(circ_bell,backend,shots=1024)
result_bell=job_bell.result()

counts_bell=result_bell.get_counts()
plot_histogram(counts_bell)

#Import the random circuit class
from qiskit.circuit.random import random_circuit
#Create two random circuits
qc1 = random_circuit(2,2) #2 qubits, 2 layers of gates (in this case one layer: two one-qubit gate or one two-qubits gate)
qc2 = random_circuit(2,4)
#Concatenate the two random circuits
qc = qc1.compose(qc2)
#Draw the circuit
qc.draw('mpl')

#Define function to print circuit properties:
def print_circuit_props(qc):
    width = qc.width() #number of gates in the layer with the most gates ("control" operation is counted as a gate (Ex. cx(0,1) width=2, one gate on qubit 0 and one on qubit 1), 
    #also the idenity (do nothing) in the layer with the most gates is counted as one gate, see the next example)
    depth = qc.depth() #number of layers in witch we can partition the circuit
    num_operators = qc.count_ops() #gives the list of operations with their number of applications
    circuit_size = qc.size() #number of gates of the circuit
    print('Width = ',width) 
    print('Depth = ', depth)
    print('Circuit size = ',circuit_size)
    print('Number of operators = ', num_operators)

#Pass our quantum circuit to print out the circuit properties
print_circuit_props(qc)

qc = QuantumCircuit(3)
qc.cx(0,1)
print_circuit_props(qc)
qc.draw('mpl')

qc = QuantumCircuit(3)
qc.ccx(0,1,2)
#Print out the circuit properties
print_circuit_props(qc)
qc.draw('mpl')

qc.decompose().draw('mpl')

#Print out the circuit properties
print_circuit_props(qc.decompose())

#Create a custom two-qubit composite gate
#Create the quantum register
qr = QuantumRegister(2, name='qr_c') #name parameter gives a name to qubits
#Generate quantum circuit which will make up the composite gate
comp_qc = QuantumCircuit(qr, name='my-composite') #my-composite is the name of the circuit 
#Add any gates you wish to your composite gate
comp_qc.h(0)
comp_qc.cx(0, 1)
#Create the composite instructions by converting the QuantumCircuit to a list of Instructions
composite_inst = comp_qc.to_instruction()
#Draw the circuit which will represent the composite gate
comp_qc.draw('mpl')

#Create another 3-qubit circuit 
qr2 = QuantumRegister(3, 'qr')
#Create a quantum circuit using the quantum register
qc = QuantumCircuit(qr2)
#Add any arbitrary gates that would represent the function of the composite gate 
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
#Draw the composite circuit
qc.draw('mpl')

#Append your composite gate to the specified qubits. 
qc.append(composite_inst, [qr2[0], qr2[1]])
#Draw the complete circuit
qc.draw('mpl')

qr=QuantumRegister(2,'qr')
circ=QuantumCircuit(qr,name='bell_minus')
circ.x(0)
circ.h(0)
circ.cx(0,1)

bell_minus = circ.to_instruction() 
circ.draw('mpl')

qr_new=QuantumRegister(2,'qr_new')
circ1=QuantumCircuit(qr_new)
circ1.append(bell_minus, [qr_new[0], qr_new[1]])
circ1.draw('mpl')

theta = Parameter('θ')

n = 5

qc = QuantumCircuit(5, 1)

qc.h(0)
for i in range(n-1):
    qc.cx(i, i+1)

qc.barrier()
qc.rz(theta, range(5)) #theta is the angle of rotation range(5) is a way to apply rz to qubits from 0 to 4
qc.barrier()

for i in reversed(range(n-1)):
    qc.cx(i, i+1)
qc.h(0)
qc.measure(0, 0)


qc.draw('mpl')

theta_range = np.linspace(0, 2 * np.pi, 128) #returns a list of 128 values equispaced in[0, 2pi]

circuits = [qc.bind_parameters({theta: theta_val}) for theta_val in theta_range] 
#circuits is a list in whitch each element is the qc circuit with the parameter substituted by a value of theta_range
#Ex. circuit[0] = qc with theta=0       circuit[-1] = qc with theta=2pi

circuits[-1].draw('mpl')

from qiskit import BasicAer, transpile
backend = BasicAer.get_backend('qasm_simulator')
job = backend.run(transpile(circuits, backend)) #we are running all the circuits contained in circuits list
#transpile(circ,syst) optimizes the circuit for the system

#Transpilation is the process of rewriting a given input circuit to match the topology of a specific quantum device, 
# and/or to optimize the circuit for execution on present day noisy quantum systems.
#Most circuits must undergo a series of transformations that make them compatible with a given target device, 
# and optimize them to reduce the effects of noise on the resulting outcomes.
# Rewriting quantum circuits to match hardware constraints and optimizing for performance can be far from trivial. 
# The flow of logic in the rewriting tool chain need not be linear, and can often have iterative sub-loops, conditional branches, and other complex behaviors.
#https://docs.quantum.ibm.com/api/qiskit/transpiler

counts = job.result().get_counts()

plot_histogram(counts) #this are the counts of measurement of the first qubit of all the elements of circuits list

job_zero = backend.run(transpile(circuits[0], backend)) #theta = 0
counts_zero = job_zero.result().get_counts()
plot_histogram(counts_zero)

job_20 = backend.run(transpile(circuits[20], backend)) #theta = 0.9894780011306435
counts_20 = job_20.result().get_counts()
plot_histogram(counts_20)

import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8,6))
ax = fig.add_subplot(111) #to create subplot of the figure fig

#The argument (111) is a shorthand for a subplot grid specification. In this case:

#The first digit represents the number of rows in the grid.
#The second digit represents the number of columns in the grid.
#The third digit represents the index of the subplot.

ax.plot(theta_range, list(map(lambda c: c.get('0', 0), counts)), '.-', label='0')
ax.plot(theta_range, list(map(lambda c: c.get('1', 0), counts)), '.-', label='1') 

#lambda c:c.get('0',0), c is the input of the function, c.get('0',0) returns the value associated to the key '0', the other 0 is the default value
#The use of map() and lambda functions is one way to apply a function to each element of an iterable
#so map(lambda c: c.get('0', 0), counts) returns the number of '0' in each element of counts


ax.set_xticks([i * np.pi / 2 for i in range(5)])
ax.set_xticklabels(['0', r'$\frac{\pi}{2}$', r'$\pi$', r'$\frac{3\pi}{2}$', r'$2\pi$'], fontsize=14)
ax.set_xlabel('θ', fontsize=14)
ax.set_ylabel('Counts', fontsize=14)
ax.legend(fontsize=14)

p = Parameter('p')
qc = QuantumCircuit(3, name='oracle')
qc.rz(p, 0)
qc.cx(0, 1)
qc.rz(p, 1)
qc.cx(1, 2)
qc.rz(p, 2)

theta = Parameter('theta')
phi = Parameter('phi')
gamma = Parameter('gamma')

qr = QuantumRegister(9)
larger_qc = QuantumCircuit(qr)
larger_qc.append(qc.to_instruction({p: theta}), qr[0:3])
larger_qc.append(qc.to_instruction({p: phi}), qr[3:6])
larger_qc.append(qc.to_instruction({p: gamma}), qr[6:9])

print(larger_qc.draw())

print(larger_qc.decompose().draw())

# Will execute the circuit on the state vector (sv) simulator
# Returns state vector results, circuit diagram, and Bloch sphere 
from qiskit import Aer, execute
from qiskit.tools.jupyter import *
from qiskit.visualization import *
def execute_circuit_sv(quantum_circuit):
    #Create a state vector simulator
    statevector_simulator = Aer.get_backend('statevector_simulator')
    #Execute the circuit on the simulator
    result = execute(quantum_circuit, statevector_simulator).result()
    #Assign state vector results
    statevector_results  = result.get_statevector(quantum_circuit)
    #Draw the circuit diagram
    circuit_diagram = quantum_circuit.draw('mpl')
    #Draw the Bloch sphere result
    q_sphere = plot_state_qsphere(statevector_results)
    #Return the results, circuit diagram, and Bloch sphere
    return statevector_results, circuit_diagram, q_sphere

#H-gate 
#Create the single qubit circuit
qc = QuantumCircuit(1)
#Add an H gate to the qubit
qc.h(0)
#Execute the circuit and capture all the results
result, img, qsphere  = execute_circuit_sv(qc)

qsphere

#U3-gate
from math import pi
#Create a single qubit circuit
qc = QuantumCircuit(1)
#Add a U3 gate and rotate all parameters by pi/2, and apply it to the qubit
qc.u(pi/2, pi/2, pi/2, 0) #U3(theta, phi, lambda) executes a rotation of theta around x-axis, of phi around z-axis and pf lambda around y-axis
#Execute the circuit and capture all the results
result, img, qsphere = execute_circuit_sv(qc)

qsphere

import qiskit_ibm_provider
# Save you credential on disc
#IBMProvider.save_account("Your token", overwrite= True)

from qiskit_ibm_provider import IBMProvider
provider = IBMProvider()

from qiskit_ibm_provider import IBMProvider
provider = IBMProvider(token=<INSERT_IBM_QUANTUM_TOKEN>)

print(provider.backends())

# We use IBMQ ibm_brisbane
device_backend = provider.get_backend('ibm_brisbane')

# Display device configuration.

config = device_backend.configuration()
print("This backend is called {} (version {}) with {} qubit{}. "
      "\nThe basis gates supported on this device are {}."
      "".format(config.backend_name,
                config.backend_version,
                config.n_qubits,
                '' if config.n_qubits == 1 else 's',
                config.basis_gates))

#from qiskit.visualization import plot_coupling_map
#qubit_coordinates = [[0, 0], [0, 1], [0, 2], [1, 1], [2, 1]]

#plot_coupling_map(config.n_qubits, qubit_coordinates, config.coupling_map)

props = device_backend.properties()

def display_qubit_info(qubit, properties):
    """Print a string describing some of reported properties of the given qubit."""

    # Conversion factors from standard SI units
    us = 1e6
    ns = 1e9

    print("Qubit {0} has a \n"
          "  - T1 time of {1:.2f} microseconds\n"
          "  - T2 time of {2:.2f} microseconds\n"
          "  - U2 gate error of {3:.2e}\n"
          "  - Readout error of {4:.2e} ".format(
              qubit,
              properties.t1(qubit) * us,
              properties.t2(qubit) * us,
              properties.gate_error('sx', qubit),
              properties.readout_error(qubit)))

display_qubit_info(0, props)

from qiskit import QuantumCircuit
from qiskit.compiler import transpile
from qiskit.transpiler import PassManager

from qiskit.transpiler import CouplingMap, Layout
from qiskit.transpiler.passes import BasicSwap, LookaheadSwap, StochasticSwap

coupling = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6]]

circuit = QuantumCircuit(7)
circuit.h(3)
circuit.cx(0, 6)
circuit.cx(6, 0)
circuit.cx(0, 1)
circuit.cx(3, 1)
circuit.cx(3, 0)

coupling_map = CouplingMap(couplinglist=coupling)

bs = BasicSwap(coupling_map=coupling_map)
pass_manager = PassManager(bs)
basic_circ = pass_manager.run(circuit)

ls = LookaheadSwap(coupling_map=coupling_map)
pass_manager = PassManager(ls)
lookahead_circ = pass_manager.run(circuit)

ss = StochasticSwap(coupling_map=coupling_map)
pass_manager = PassManager(ss)
stochastic_circ = pass_manager.run(circuit)

circuit.draw(output='mpl')

lookahead_circ.draw(output='mpl')

import math
from qiskit.providers.fake_provider import FakeTokyo

backend = FakeTokyo()  

optimized_0 = transpile(circuit, backend=backend, seed_transpiler=11, optimization_level=0)

optimized_0.draw('mpl')

qc = random_circuit(3,2)
qc.measure_all()

# Select a backend to run the circuit
backend = provider.get_backend('ibmq_qasm_simulator')

result = backend.run(transpile(qc, backend), shots=1000).result()
counts  = result.get_counts(qc)
print(counts)

# Plot the result
plot_histogram(counts, color='green', title="New Histogram")

## Insert your code here
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_bloch_multivector, plot_histogram



#INSERT CODE HERE

## CHECK THE RESULT
from qiskit.visualization import plot_bloch_multivector, plot_histogram, array_to_latex
display(array_to_latex(Statevector.from_instruction(qc), prefix="\\text{Statevector} = "))

qc = QuantumCircuit(2,2) # this time, we not only want two qubits, but also two classical bits for the measurement
#INSERT CODE HERE

## WRITE YOUR CODE HERE:
#
#
#
#


backend = Aer.get_backend('qasm_simulator') # we choose the simulator as our backend
counts = execute(qc, backend, shots = 1000).result().get_counts() # we run the simulation and get the counts
counts

## WRITE YOUR CODE/calculation HERE:
#
#
#
#

# inverse circuit


#BONUS VISUALIZATION
gate_colours = {
    "displaycolor": {
        "x": [ # gate name
            "#da1e28", # box color (red)
            "#FFFFFF" # box text color (white)
        ],
        "h": [ # gate name
            "#da1e28", # box color (red)
            "#FFFFFF" # box text color (white)
        ],
        "cx": [ # gate name
            "#da1e28", # box color (red)
            "#FFFFFF" # box text color (white)
        ],
        "ry": [ # gate name
            "#da1e28", # box color (red)
            "#FFFFFF" # box text color (white)
        ],
    },
}

display(qci.draw('mpl',style=gate_colours)) 



from qiskit import QuantumCircuit
import qiskit.quantum_info as qi

qc_AB = QuantumCircuit(2)
qc_AB.h(0)
qc_AB.cx(0,1)
qc_AB.draw('mpl')

psi_AB = qi.Statevector.from_instruction(qc_AB)
psi_AB.draw('latex', prefix='|\\psi_{AB}\\rangle = ')

rho_AB = qi.DensityMatrix.from_instruction(qc_AB)
rho_AB.draw('latex', prefix='\\rho_{AB} = ')

from qiskit.visualization import plot_state_city
plot_state_city(rho_AB.data, title='Density Matrix')

import numpy as np
rho_p = qi.DensityMatrix.from_label('+')
display(rho_p.draw('latex', prefix='\\rho_p = '))
gamma_p = rho_p.purity()
print("State purity: ", np.round(np.real(gamma_p),3))

rho_m = 1/2*(qi.DensityMatrix.from_label('0') + qi.DensityMatrix.from_label('1'))
display(rho_m.draw('latex', prefix='\\rho_m = '))
gamma_m = rho_m.purity()
print("State purity: ", np.round(np.real(gamma_m),3))

rho_B = qi.partial_trace(rho_AB,[0])
rho_A = qi.partial_trace(rho_AB,[1])

display(rho_B.draw('latex', prefix=" \\rho_{B} = "),
        rho_A.draw('latex', prefix=" \\rho_{A} = "))

qc_CD = QuantumCircuit(2)
qc_CD.ry(np.pi/3,0)
qc_CD.h(1)
qc_CD.cx(0,1)
qc_CD.cx(1,0)
qc_CD.draw("mpl", style="iqx")

from qiskit import *
from qiskit.circuit import QuantumCircuit
from qiskit.quantum_info import partial_trace, entropy

import numpy as np
import math

nQubits = # <--- insert your code here
nClassicBits = 4

circuit.draw(output='mpl')

# Run the quantum circuit on a statevector simulator backend
backend = # <--- insert your code here

outputstate = # <--- insert your code here
print(outputstate)

list_check = [5,7,8,10]
for ind in list_check:
    print(outputstate[ind])

def check_vn_entropy(vn_entropy):
    if vn_entropy != 0.:
        print("The state is a mixed state")

## insert your code here -->
check_vn_entropy(my_entropy)