#in general we can transform a single CNOT inot a controlled
#version of any rotaion around the Bloch sphere by an angle pi,
#by simply preceding and following it with the correct rotations.
%matplotlib inline
# Importing standard Qiskit libraries and configuring account
from qiskit import QuantumCircuit, execute, Aer, IBMQ
from qiskit.compiler import transpile, assemble
from qiskit.tools.jupyter import *
from qiskit.visualization import *
from qiskit.circuit import Gate
from math import pi
# Loading your IBM Q account(s)
provider = IBMQ.load_account()

qc = QuantumCircuit(2)
c =0
t =1

#built in controlled-Z
#recall a Z-GATE converts a |+> qubit to a |-> and vive-versa 
qc.cz(c,t)
qc.draw()


#converting a CNOT into a CONTROLLED-Z
qc = QuantumCircuit(2)
qc.h(t)
qc.cx(c,t)
qc.h(t)
qc.draw()

#CONTROLLED-Y GATE.
qc = QuantumCircuit(2)
qc.sdg(t)
qc.cx(c,t)
qc.s(t)
qc.draw()

#CONTROLLED-H GATE
qc = QuantumCircuit(2)
qc.ry(pi/4,t)
qc.cx(c,t)
qc.ry(-pi/4,t)
qc.draw()

#SWAPPING QUBITS:
#sometime we need to move information around in a quantum Computer.
#one option is to move the state between two qubits.
a = 0
b = 1
#swaps states oc qubits a and b
qc = QuantumCircuit(2)
qc.swap(a,b)
qc.draw()

# we will now develope a swap gate using he base gate.
#first we will look at the |1> and |0> cases:

#swaps 1 from a to b:
# puts qubits b in state |1> and qubit a in state |0> 
qc = QuantumCircuit(2)
qc.cx(a,b) #copies 1 from a to b
qc.cx(b,a) #uses the 1 on b to rotate the state of a to 0
qc.draw()

#swap back to original state
qc.cx(b,a)
qc.cx(a,b)
qc.draw()

#more  general swap gate:
# works for |00>,|01>,|10> and |11>
#note you can change the order of the CNOT gates and get the same results
qc = QuantumCircuit(2)
qc.cx(b,a)
qc.cx(a,b)
qc.cx(b,a)
qc.draw()

#controlled Rotations:
#this circuit rotates qubits about the Y-AXIS by theta/2

qc = QuantumCircuit(2)
theta = pi # theta can be any value
qc.ry(theta/2,t)
qc.cx(c,t)
qc.ry(-theta/2,t)
qc.cx(c,t)
qc.draw()


# we can also create a controlled version of any single
#qubit rotation U
# We must first find three rotations A,B,and C, 
#and a phase alpha such that 
# ABC = I , e^(ialpha)AZBZC =U

A = Gate('A',1,[])
B = Gate('B',1,[])
C = Gate('C',1,[])
alpha = 1 #arbitratily define alpha
qc = QuantumCircuit(2)
qc.append(C,[t])
qc.cz(c,t)
qc.append(B,[t])
qc.cz(c,t)
qc.append(A,[t])
qc.u1(alpha,c)
qc.draw()

#TOFFOLI:
#The Toffoli is a 3-qubit gate with two control and one target.
#It performs an X on the target only if both controls
# are in state |1>.
# the final state of target is then either the AND or NAND of the two controls,
# depending on if the initial state of target was |0> or |1>.
# the Toffoli can be thought of as a controlled-controoled-NOT:
# note the Toffoli requires at least 6 CNOT gates.
# but there are other ways to produce an AND effect using quantum operations.
qc = QuantumCircuit(3)
a = 0
b = 1
t = 2
#Toffoli with control a and b target t:
qc.ccx(a,b,t)
qc.draw()


# before we built a Toffoli from base quibit operations
# we will build a general controlled-controlled-U.
#For any single-qubit rotation U.
qc = QuantumCircuit(3)
qc.cu1(theta,b,t)
qc.cx(a,b)
qc.cu1(-theta,b,t)
qc.cx(a,b)
qc.cu1(theta,a,t)
qc.draw()

#we can built an AND gate using a CONTROLLED-H GATE and CONTROLLED-Z GATES
qc = QuantumCircuit(3)
qc.ch(a,t)
qc.cz(b,t)
qc.ch(a,t)
qc.draw()
