# run this cell if you're executing this notebook in your browser
!pip install qiskit

from IPython.display import clear_output
clear_output()

from qiskit import *
import numpy as np

qc = QuantumCircuit(2,2)

qc.draw('mpl')

qreg = QuantumRegister(2, name = 'qreg')
creg = ClassicalRegister(2, name = 'creg')

qc = QuantumCircuit(qreg, creg)

qc.draw('mpl')

qc = QuantumCircuit(1)

# set the angles to whatever you want
theta = np.pi/2
phi = np.pi/4
lambdaa = np.pi

# comment/uncomment the gates to play with them

qc.x(0) # x on qubit 0
qc.y(0) # y on qubit 0
qc.z(0) # z on qubit 0
qc.s(0) # s gate on qubit 0, sqrt of z
qc.sdg(0) # s† on qubit 0
qc.t(0) # t gate on qubit 0, sqrt of s
qc.tdg(0) # t† on qubit 0

### rotations
qc.rx(theta,0) # rx rotation on qubit 0
qc.ry(theta,0) # ry rotation on qubit 0
qc.rz(theta,0) # rz rotation on qubit 0

### generic
qc.u(theta, phi, lambdaa, 0) #u3 gate
qc.u2(phi, lambdaa, 0) #u2 = u3(pi/2, phi, lambdaa)
qc.p(lambdaa, 0) #p = u1 = u3(0, 0, lambdaa)

#

display(qc.draw('mpl'))
display(qc.draw())


qc = QuantumCircuit(2)

# set the angles to whatever you want
theta = np.pi/2
phi = np.pi/4
lambdaa = np.pi


# comment/uncomment any of the following to your heart's content

qc.cx(0,1) # CNOT with qubit 0 as control and qubit 1 as target
qc.cy(0,1) # controlled-Y with qubit 0 as control and qubit 1 as target
qc.cz(0,1) # controlled-Z with qubit 0 as control and qubit 1 as target
qc.ch(0, 1) # controlled-H with qubit 0 as control and qubit 1 as target
qc.cu3(theta, phi, lambdaa, 0, 1) # controlled-u3 with qubit 0 as control and qubit 1 as target
qc.swap(0,1) # swap qubits 0 and 1

#

qc.draw('mpl')


from qiskit.circuit.library import CXGate

qc = QuantumCircuit(3)

# Method 1:
qc.ccx(0,1,2) 


# Method 2:
qc.mct([0,1],2)

#Method 3
our_ccx = CXGate().control()
qc.append(our_ccx, [0,1,2])


qc.draw('mpl')


qc_bell = QuantumCircuit(2)

qc_bell.h(0)
qc_bell.cx(0,1)
qc_bell.x(1)
qc_bell.z(1)

display(qc_bell.draw('mpl'))

from qiskit.quantum_info import Statevector 

state = Statevector(qc_bell)

state.draw('latex', prefix = '\\left|\\psi^-\\right\\rangle = ' )

bell_qasm_string = """
OPENQASM 2.0; 
include "qelib1.inc"; 

gate bell a, b {
    u(pi/2, 0, pi) a;
    cx a, b;
}

qreg q[2];
creg c[2];

bell q[0], q[1];
x q[1];
z q[1];

barrier q[0],q[1];
measure q[0] -> c[0];
measure q[1] -> c[1];
"""

qc_bell_qasm = QuantumCircuit.from_qasm_str(bell_qasm_string)
qc_bell_qasm.draw('mpl')

qc_bell_string = qc_bell.qasm()
print(qc_bell_string)

qc_bell_init = QuantumCircuit(2)
qc_bell_init.h(0)
qc_bell_init.cx(0,1)

display(qc_bell_init.draw('mpl'))

bell_gate = qc_bell_init.to_gate()
bell_gate.name ='Bell Gate'

qc_bell_gate = QuantumCircuit(2)
qc_bell_gate.append(bell_gate, [0,1])

display(qc_bell_gate.draw('mpl'))

from qiskit.quantum_info import Operator
from qiskit.visualization import array_to_latex

U = Operator(qc_bell)

array_to_latex(U.data, prefix = 'U = ')

from qiskit.providers.aer import UnitarySimulator

backend = UnitarySimulator() # alternative: backend = Aer.get_backend('unitary_simulator')

job = execute(qc_bell, backend)
U = job.result().get_unitary()

display(array_to_latex(U.data, prefix = 'U = '))



from qiskit.providers.aer import QasmSimulator

qc_measure = QuantumCircuit(2,2)

qc_measure.barrier()
qc_measure.measure([0,1], [0,1]) # alternative: qc_measure = QuantumCircuit(2) | qc_measure.measure_all() = .barrier() + measure every qubit + creates a classical register

qc_bell_measure = qc_measure.compose(qc_bell, range(2), front = True) # combines two circuits, placing qc_bell in front of qc_measure

display(qc_bell_measure.draw('mpl'))

backend = QasmSimulator() # alternative: backend = Aer.get_backend('qasm_simulator')

job = execute(qc_bell_measure, backend, shots = 1024) # alternative: qc_transpiled = transpile(qc_bell_measure, backend), job = backend.run(qc_transpiled, shots = 1024)

counts = job.result().get_counts()

print('Counts: ' + str(counts))


from qiskit.visualization import plot_histogram

plot_histogram(counts)

job = execute(qc_bell_measure, backend, shots = 1024) 

counts_2 = job.result().get_counts()

print('Counts, second experiment: ' + str(counts_2))

legend = ['First Experiment', 'Second Experiment']
plot_histogram([counts, counts_2], legend = legend, color = ['teal', 'black'], figsize = (10, 7), bar_labels = False)

qc_ghz = QuantumCircuit(3)

qc_ghz.h(0)
qc_ghz.cx(0,1)
qc_ghz.ccx(0,1,2)
qc_ghz.draw('mpl') #original circuit

display(qc_ghz.decompose().draw('mpl')) #decompose one level down
display(qc_ghz.decompose().decompose().draw('mpl')) #decompose two levels down

from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import Unroller

gates_pass = Unroller(['u1', 'u2', 'u3', 'cx']) #play around with this
pm = PassManager(gates_pass)
pm.run(qc_ghz).draw('mpl')

print('The circuit depth of the Bell circuit without measurements is: ' + str(qc_bell.depth()))

#Qiskit counts the measurements in the depth calculation:
print('The circuit depth of the Bell circuit with measurements is: ' + str(qc_bell_measure.depth()))

from qiskit.circuit.random import random_circuit
import random

#play around with these parameters:
num_qubits = 3 
max_depth = 10
add_measurements = True #True/False

#random circuit:
rand_circ = random_circuit(num_qubits, depth = random.randint(1,max_depth), measure=add_measurements) 
rand_circ.draw('mpl')

# run this cell to see if you are correct

print('The circuit depth is: ' + str(rand_circ.depth()))

