from qiskit import QuantumCircuit, transpile, Aer
from qiskit.visualization import plot_histogram
import numpy as np
sim = Aer.get_backend('aer_simulator')

def U_f1(qc):
    
    return qc

def U_f2(qc):
    
    qc.x(1) #Compuerta X al segundo qubit
    
    return qc

def U_f3(qc):
    
    qc.cx(0,1) #Compuerta CNOT entre primer y segundo quibit (el primero es el que controla)
    return qc

def U_f4(qc):   
    
    qc.cx(0,1) #Compuerta CNOT entre primer y segundo quibit (el primero es el que controla)
    qc.x(1) #Compuerta X al segundo qubit
    
    return qc

def Deutsch(U_f):
    
    qc=QuantumCircuit(2,1) #Se crea un circuito cuántico con 2 bits cuánticos y 1 canal clásico
    qc.x(1) #Compuerta X al segundo qubit (inicializar estado |1>)
    qc.h(0) #Compuerta H al primer qubit
    qc.h(1) #Compuerta H al segundo qubit
    qc.barrier() #Barrera (empieza oráculo)
    qc = U_f(qc) #Agregamos el oráculo
    qc.barrier() #Barrera (termina oráculo)
    qc.h(0) #Compuerta H al primer qubit
    qc.measure(0,0) #Medimos el primer qubit y enviamos señal al canal clásico

    return qc

qc=Deutsch(U_f1) # definición circuito con oráculo usando f_1(x)
display(qc.draw()) # visualización del circuito

counts = sim.run(qc).result().get_counts()  #contando las medidas de simulador cuántico
plot_histogram(counts) #histrograma de resultados

qc=Deutsch(U_f2) #definición circuito con oráculo usando f_2(x)
display(qc.draw()) # visualización del circuito

counts = sim.run(qc).result().get_counts() #contando las medidas del simulador cuántico   
plot_histogram(counts) #histrograma de resultados

qc=Deutsch(U_f3) #definición circuito con oráculo usando f_3(x)
display(qc.draw()) # visualización del circuito

counts = sim.run(qc).result().get_counts()  #contando las medidas de simulador cuántico
plot_histogram(counts) #histrograma de resultados

qc=Deutsch(U_f4) #definición circuito con oráculo usando f_4(x)
display(qc.draw()) # visualización del circuito

counts = sim.run(qc).result().get_counts()  #contando las medidas de simulador cuántico
plot_histogram(counts) #histrograma de resultados

#oráculo para f(x) constante para un número n de bits en el registro
def constant(qc,n):
    
    ran=np.random.randint(2) #selección aleatoria de 0 ó 1
    if ran == 1: 
        qc.x(n) #si el número aleatorio es 1 se pone compuerta X en el objetivo (se induce fase global -1 al registro)
        
    return qc

#oráculo para f(x) balanceado para un número n de bits en el registro
def balanced(qc,n):
    
    for i in range(n):
        qc.cx(i,n) #se crea una CNOT entre cada qubit del registro y el objetivo (los qubits del registro controlan)
    ran=np.random.randint(2) #selección aleatoria de 0 ó 1
    if ran == 1:
        qc.x(n) #si el número aleatorio es 1 se pone compuerta X en el objetivo (se induce fase global -1 al registro)
    
    return qc

def D_J(U_f,n):
    
    qc=QuantumCircuit(n+1,n) #Se crea un circuito cuántico con n+1 quibits y n canales clásicos
    qc.x(n) #Compuerta X al bit del registro
    for i in range(n+1):
        qc.h(i) #Compuerta H a todos los bits 
    qc.barrier() #Barrera (empieza oráculo)
    qc = U_f(qc,n) #Agregamos el oráculo
    qc.barrier() #Barrera (termina oráculo)
    for i in range(n):
        qc.h(i)  #Compuerta H a los n bits del registro
        qc.measure(i,i) #Medición los n bits del registro
        
    return qc   

qc=D_J(constant,3) #definición circuito con oráculo constante y 3 bits en registro
display(qc.draw()) #ver circuito

counts = sim.run(qc).result().get_counts() #contando las medidas de simulador cuántico     
plot_histogram(counts) #histrograma de resultados

qc=D_J(balanced,3)
display(qc.draw())

counts = sim.run(qc).result().get_counts()  
plot_histogram(counts)

