# required import of libraries and modules

# import array to latex converter from
# IBM's Qiskit visualization module
from qiskit.visualization import array_to_latex

# import state-vector and random state-vector from
# IBM's Qiskit quantum information module
from qiskit.quantum_info import Statevector, random_statevector

# import abstract operator and pauli operator from
# IBM's Qiskit quantum information's operators module
from qiskit.quantum_info.operators import Operator, Pauli

# import quantum circuit from IBM's Qiskit
from qiskit import QuantumCircuit

# import hadamard gate and CX/CNOT gate from IBM's Qiskit
from qiskit.circuit.library import HGate, CXGate

# import numpy
import numpy as np

# define the column-vector for |0>
ket0 = [[1],[0]]

# convert the column-vector |0> to latex
array_to_latex(ket0)

# define the row-vector for <0|
bra0 = [1, 0]

# convert the row-vector <0| to latex
array_to_latex(bra0)

# put your answer answer here for the column-vector |1>
ket1 = [ [0], [1] ]

# put your answer here for the row-vector <1|
bra1 = [0, 1]

# import the grader for the exercise 1 of the lab 1
from qc_grader.challenges.qgss_2023 import grade_lab1_ex1 

# grade the exercise 1 of the lab 1
grade_lab1_ex1([ket1, bra1])

# create a state-vector for the row-vector <0|
sv_bra0 = Statevector(bra0)

# show the object of the state-vector for the row-vector <0|
sv_bra0

# draw the state-vector for the row-vector <0| in latex
sv_bra0.draw("latex")

# create a complex state-vector for multiple qubits 
sv_eq = Statevector( [ 1/2, 3/4, 4/5, 6/8 ] )

# draw the complex state-vector created before in latex
sv_eq.draw("latex")

# check if the complex state-vector created before
# is valid for a quantum state (i.e., if it is normalised)
sv_eq.is_valid()

# create your valid statevector here
sv_valid = Statevector( [ 1/2, 1/2, 1/2, 1/2 ] )

# import the grader for the exercise 2 of the lab 1
from qc_grader.challenges.qgss_2023 import grade_lab1_ex2 

# grade the exercise 2 of the lab 1
grade_lab1_ex2(sv_valid)

# create an operator for the row-vector <0|
op_bra0 = Operator(bra0)

# show the object of the operator for the row-vector <0|
op_bra0

# create an operator for the column-vector |0>
op_ket0 = Operator(ket0)

# apply the tensor product of the operator
# created before to the operator applied to
# the row-vector <0|
op_bra0.tensor(op_ket0)

# compute the inner product between the row-vector <0|
# and the column-vector |0>, as a bra-ket product <0|0>
# note: this operator is also used to check if
#       two pure quantum states overlap (i.e., are equal)
braket = np.dot(op_bra0, op_ket0)

# convert the result of the inner product
# computed before to latex
array_to_latex(braket)

# compute the outer product between the column-vector |0>
# and the column-vector <0|, as a ket-bra product |0><0|
ketbra = np.outer(ket0,bra0)

# convert the result of the outer product
# computed before to latex
array_to_latex(ketbra)

# definition of the operators for the row-vectors (bra vectors)
# <0| and <1|, and the column-vectors (ket vectors) |0> and |1>
op_bra0 = Operator(bra0)
op_bra1 = Operator(bra1)
op_ket0 = Operator(ket0)
op_ket1 = Operator(ket1)


# definition of the inner products (bra-ket products)

# put your answer for the inner product
# (bra-ket product) <1|0> here
bra1ket0 = np.dot(op_bra1, op_ket0)

# put your answer for the inner product
# (bra-ket product) <0|1> here
bra0ket1 = np.dot(op_bra0, op_ket1)

# put your answer for the inner product
# (bra-ket product) <1|1> here
bra1ket1 = np.dot(op_bra1, op_ket1)


# definition of the outer products (ket-bra products)

# put your answer for the outer product
# (ket-bra product) |1><0| here
ket1bra0 = np.outer(op_ket1, op_bra0)

# put your answer for the outer product
# (ket-bra product) |0><1| here
ket0bra1 = np.outer(op_ket0, op_bra1)

# put your answer for the outer product
# (ket-bra product) |1><1| here
ket1bra1 = np.outer(op_ket1, op_bra1)

# import the grader for the exercise 3 of the lab 1
from qc_grader.challenges.qgss_2023 import grade_lab1_ex3 

# grade the exercise 3 of the lab 1
grade_lab1_ex3( [ bra1ket0, bra0ket1, bra1ket1, ket1bra0, ket0bra1, ket1bra1 ] )

# two quantum states are orthogonal if
# they are completely opposite and
# never overlap with each other
# (i.e., when their inner product is equal to 0)

# <0|1> = <1|0> = 0, the quantum states |0> and |1>
# never overlap with each other and are completely opposite,
# or in other words, they are orthogonal

# add or remove your answer from this list
answer = ["a"]

# import the grader for the exercise 4 of the lab 1
from qc_grader.challenges.qgss_2023 import grade_lab1_ex4 

# grade the exercise 4 of the lab 1
grade_lab1_ex4(answer)

# create an operator for a single qubit system defined as
# m_1 = [ [1, 1],
#         [0, 0] ], which represents the f1 = constant-0
#                   deterministic operation
m1 = Operator( [ [1, 1], [0, 0] ] )

# convert the operator created before to latex
array_to_latex(m1)

# create an operator for a single qubit system defined as
# m_3 = [ [0, 1],
#         [1, 0] ], which represents the f3 = bit flip / not
#                   deterministic operation
m3 = Operator( [ [0, 1], [1,0] ] )

# convert the operator created before to latex
array_to_latex(m3)

# represent in latex the result of applying
# the operator m1 to the column-vector (ket vector) |0>
array_to_latex(m1@ket0)

# create an operator for a single qubit system defined as
# m_2 = [ [1, 0],
#         [0, 1] ], which represents the f2 = identity
#                   deterministic operation
m2 = Operator( [ [1, 0], [0, 1] ] )

# create an operator for a single qubit system defined as
# m_4 = [ [0, 0],
#         [1, 1] ], which represents the f4 = constant-1
#                   deterministic operation
m4 = Operator( [ [0, 0], [1, 1] ] )

# import the grader for the exercise 5 of the lab 1
from qc_grader.challenges.qgss_2023 import grade_lab1_ex5

# grade the exercise 5 of the lab 1
grade_lab1_ex5( [ m2, m4 ] )

# create a CX/CNOT quantum gate as an operator
cnot = CXGate()

# convert the operator created before to latex
array_to_latex(cnot)

# check if the operator for a single qubit system defined as
# m_3 = [ [0, 1],
#         [1, 0] ], which represents the f3 = bit flip / not
#                   deterministic operation, is unitary
m3.is_unitary()

# create a random operator for multiple qubit system
random = Operator( np.array( 
    [ [  0.50778085 - 0.44607116j, -0.1523741  + 0.14128434j, 
         0.44607116 + 0.50778085j, -0.14128434 - 0.1523741j  ],
      [  0.16855994 + 0.12151822j,  0.55868196 + 0.38038841j,
        -0.12151822 + 0.16855994j, -0.38038841 + 0.55868196j ],
      [  0.50778085 - 0.44607116j, -0.1523741  + 0.14128434j,
        -0.44607116 - 0.50778085j,  0.14128434 + 0.1523741j  ],
      [  0.16855994 + 0.12151822j,  0.55868196 + 0.38038841j,
         0.12151822 - 0.16855994j,  0.38038841-0.55868196j ] ] ) )

# check if the random operator for multiple qubit system is random
random.is_unitary()

# create your non-unitary operator here
non_unitary_op = Operator( [ [1, 0], [1, 1] ] )

# import the grader for the exercise 6 of the lab 1
from qc_grader.challenges.qgss_2023 import grade_lab1_ex6

# grade the exercise 6 of the lab 1
grade_lab1_ex6(non_unitary_op)

# create the Pauli-X (bit flip / not) quantum gate
pauli_x = Pauli("X")

# convert the quantum gate created before to latex
array_to_latex(pauli_x)

# create the Pauli-Y quantum gate
pauli_y = Pauli("Y")

# convert the quantum gate created before to latex
array_to_latex(pauli_y)

# create the Pauli-Z (phase flip) quantum gate
pauli_z = Pauli("Z")

# convert the quantum gate created before to latex
array_to_latex(pauli_z)

# convert the Pauli-X (bit flip / not)
# quantum gate to an operator
op_x = Operator(pauli_x)

# show the Pauli-X (bit flip / not)
# quantum gate converted to an operator
op_x

# apply the inner product between the Pauli-X (bit flip / not) operator
# the quantum state of the column-vector (ket vector) |0>, resulting to
# a new quantum state of the column-vector (ket vector) |1>
op_new = np.dot(op_x ,ket0)

# convert the resulting new operator to latex
array_to_latex(op_new)

# do your operations here

# convert the Pauli-Z (phase flip)
# quantum gate to an operator
op_z = Operator(pauli_z)

# apply the inner product between the Pauli-Z (phase flip) operator
# the quantum state of the column-vector (ket vector) |1>, resulting to
# a new quantum state of the column-vector (ket vector) -|1>
result = np.dot(op_z ,ket1)

# convert the resulting new operator to latex
array_to_latex(result)

# import the grader for the exercise 7 of the lab 1
from qc_grader.challenges.qgss_2023 import grade_lab1_ex7

# grade the exercise 7 of the lab 1
grade_lab1_ex7(result)

# create the Hadamard quantum gate
hadamard = HGate()

# convert the quantum gate created before to latex
array_to_latex(hadamard)

# convert the Hadamard quantum gate to an operator
op_h = Operator(hadamard)

# show the Hadamard quantum gate
# converted to an operator
print(op_h)

# check if the Hadamard quantum gate
# converted to an operator is unitary
op_h.is_unitary()

# create a quantum circuit for two qubits
# and to implement a Bell state
bell = QuantumCircuit(2)

# apply a Hadamard gate to
# the 1st qubit of the quantum circuit
bell.h(0)

# apply a CX/CNOT gate to
# the 1st and 2nd qubits of the quantum circuit
bell.cx(0, 1)

# draw the quantum circuit implementing the Bell state
bell.draw(output="mpl")

# converting the quantum circuit
# implementing the Bell state to an operator
bell_op = Operator(bell)

# convert the quantum operator representing
# the quantum circuit created before to latex
array_to_latex(bell_op)

# create a quantum circuit for three qubits
# and to implement a GHZ state
ghz = QuantumCircuit(3)

##############################

# add gates to your quantum circuit here

# apply a Hadamard gate to
# the quantum circuit, on the 1st qubit
ghz.h(0)

# apply a CX/CNOT gate to
# the quantum circuit,
# with control on 1st qubit
# and target on 2nd qubit
ghz.cx(0, 1)

# apply a CX/CNOT gate to
# the quantum circuit,
# with control on 2nd qubit
# and target on 3rd qubit
ghz.cx(1, 2)

##############################

# draw the quantum circuit implementing the GHZ state
ghz.draw(output="mpl")

# import the grader for the exercise 8 of the lab 1
from qc_grader.challenges.qgss_2023 import grade_lab1_ex8

# grade the exercise 8 of the lab 1
grade_lab1_ex8(ghz)

# create the state-vector for
# the quantum superposition state |+>
plus_state = Statevector.from_label("+")

# convert the state-vector created before to latex
plus_state.draw("latex")

# show the state-vector |+> created before
plus_state

# show the probabilities of the possible
# classical outcomes/states resulting 
# from measuring the quantum superposition state |+>
plus_state.probabilities_dict()

# run this cell multiple times to show collapsing
# into one state or the other (i.e., 0 or 1)

# for a number of trials equal to 5
for _ in range(5):
    
    # measure the quantum superposition state |+>
    res = plus_state.measure()
    
    # print the result of the measurement of
    # the quantum superposition state |+>
    print(res)

# create a quantum circuit for one qubit and one bit,
# to implement a |+> quantum superposition state
qc = QuantumCircuit(1, 1)

# apply a Hadamard gate to
# the quantum circuit, on the 1st qubit
qc.h(0)

# measure the 1st qubit of the quantum circuit
# to the respective 1st bit of the same
qc.measure(0, 0)

# draw the quantum circuit for one qubit and one bit,
# implementing a |+> quantum superposition state
qc.draw(output="mpl")

# create the state-vector for the Bell State |phi^+>
sv_bell = Statevector( [ np.sqrt(1 / 2), 0, 0, np.sqrt(1 / 2) ] )

# draw the state-vector for the Bell State |phi^+>
sv_bell.draw("latex")

sv_bell.probabilities_dict()

# creation of the three quantum states in the Bell basis,
# namely, |psi^+⟩, |psi^-⟩, and |phi^-⟩

# create a statevector for |psi^+⟩ here
sv_psi_plus = Statevector( [ 0, np.sqrt(1 / 2), np.sqrt(1 / 2), 0 ] )

# find the measurement probabilities for |psi^+⟩ here
prob_psi_plus = sv_psi_plus.probabilities_dict()

# create a statevector for |psi^-⟩ here
sv_psi_minus = Statevector( [ 0, np.sqrt(1 / 2), -1 * np.sqrt(1 / 2), 0 ] )

# find the measurement probabilities for |psi^−⟩ here
prob_psi_minus = sv_psi_minus.probabilities_dict()

# create a statevector for |phi^-⟩ here
sv_phi_minus = Statevector( [ np.sqrt(1 / 2), 0, 0, -1 * np.sqrt(1 / 2) ] )

# find the measurement probabilities for |phi^-⟩ here
prob_phi_minus = sv_phi_minus.probabilities_dict()

# import the grader for the exercise 9 of the lab 1
from qc_grader.challenges.qgss_2023 import grade_lab1_ex9

# grade the exercise 9 of the lab 1
grade_lab1_ex9([prob_psi_plus, prob_psi_minus, prob_phi_minus])

# create a quantum circuit for two qubits,
# to implement a quantum fourier transform
qft = QuantumCircuit(2)

##############################

# add gates to your quantum circuit here

# apply an Hadamard gate to
# the quantum circuit, on the 2nd qubit
qft.h(1)

# apply a Controlled-Phase (CP) gate to
# the quantum circuit acccording to
# a theta angle, with control on
# the 1st qubit and target on the 2nd qubit
qft.cp( np.pi/2, 0, 1 )

# apply an Hadamard gate to
# the quantum circuit, on the 1st qubit
qft.h(0)

# apply a SWAP gate to
# the quantum circuit,
# between the 1st qubit
# and the 2nd qubit
qft.swap(0, 1)

##############################

# draw the quantum circuit for two qubits,
# implementing a quntum fourier transform
qft.draw(output="mpl")

# import the grader for the exercise 10 of the lab 1
from qc_grader.challenges.qgss_2023 import grade_lab1_ex10

# grade the exercise 10 of the lab 1
grade_lab1_ex10(qft)

# create an operator U from the quantum circuit of
# the quantum fourier transform created before
U = Operator(qft)

# convert the quantum operator U representing
# the quantum circuit created before to latex
array_to_latex(U)

# import the IBM's Qiskit Jupyter Tools
import qiskit.tools.jupyter

# show the table of the IBM's Qiskit version
%qiskit_version_table