QISKIT 

Qiskit es un conjunto de herramientas de código abierto desarrollado por IBM para trabajar con computadoras cuánticas. Proporciona un marco integral que permite a los desarrolladores y científicos de datos experimentar, desarrollar y ejecutar algoritmos cuánticos en dispositivos reales o simuladores cuánticos.

Las principales características y componentes de Qiskit incluyen:

  1. Terra: Ofrece las herramientas para construir y manipular circuitos cuánticos, así como para realizar simulaciones. Terra también facilita la transpilación de circuitos para ejecutar en hardware cuántico.

  2. Aer: Es un conjunto de simuladores cuánticos de alto rendimiento incluidos en Qiskit. Permite simular el comportamiento de los circuitos cuánticos en diferentes condiciones.

  3. Ignis: Se enfoca en la caracterización y mitigación de errores en computadoras cuánticas. Proporciona herramientas para evaluar y mejorar la calidad de las implementaciones cuánticas.

  4. Aqua: Es un conjunto de bibliotecas y algoritmos cuánticos de alto nivel, diseñados para diversas aplicaciones cuánticas, como optimización, aprendizaje automático, química cuántica y finanzas cuánticas.

  5. Algoritmos cuánticos: Qiskit incluye implementaciones de algoritmos cuánticos clave, como el algoritmo de factorización de Shor, el algoritmo de búsqueda cuántica de Grover, y otros.

Qiskit es un recurso valioso para la comunidad de computación cuántica, ya que permite a los usuarios tanto principiantes como experimentados explorar el mundo de la informática cuántica y contribuir al desarrollo de aplicaciones y algoritmos cuánticos.

 

De momento, tendremos que instalar un simulador en local, probablemente acompañado de Jupyter.

Aquí tenemos las indicaciones para realizarlo: https://docs.quantum.ibm.com/start/install

Ahí mismo tienes acceso a recursos y herramientas para experimentar con computación cuántica. Puedes entrar y comenzar por "Start", "Build"... hasta terminar ejecutando programas en hardware cuántico real de IBM. 

 

CONSTRUIR Y EJECUTAR CIRCUITOS CUÁNTICOS EN UN SIMULADOR

EJECUTAR EN ORDENADORES CUÁNTICOS REALES

Hay varias tareas a realizar a la hora de ejecutar un circuito en un ordenador cuántico real:

  1. Alta en Ibm Quantum Plataform

En primer lugar, deberás darte de alta, de forma gratuita, en Ibm Quantum Plataform.

En la plataforma Ibm Quantum verás tres pestañas:

    • Dashboard, que muestra información general así como los Jobs más recientes, acceso a las QPUs utilizadas, todas las QPUs, “Documentation”, “Learning”.
    • Compute resources, que muestra todas las QPU disponibles.
    • Jobs, que muestra todos nuestros Jobs.
  1. Tendrás que ir a “manage account” para obtener tu API token.

Enlace: https://quantum.ibm.com/

  1. Será necesario que tengas instalado Qiskit en tu entorno de trabajo

Aquí tenemos las indicaciones para realizarlo: https://docs.quantum.ibm.com/start/install

  1. Ejecutar los siguientes pasos:
    • Guardar las credenciales de Ibm Quantum (solamente la primera vez).
    • Recuperar las credenciales y comprobar que todo está bien.
    • Ver backends (ordenadores cuánticos) disponibles, seleccionar el menos ocupado y crear una sesión en dicho backend
    • Construir nuestro circuito.
    • “Transpilar” nuestro circuito.
    • Ejecutarlo
    • Analizar los resultados.

 

Nota sobre: TRANSPILER

“Transpilar” un circuito cuántico es el proceso de transformar un circuito cuántico de alto nivel en una versión equivalente que sea compatible con un ordenador cuántico específico. Este proceso es crucial debido a las limitaciones físicas y las especificaciones únicas de cada ordenador cuántico. Este proceso realiza varias tareas:

  • Asegurar la compatibilidad de puertas: si el computador cuántico no soporta algún tipo de puerta, hay que descomponerla en puertas que sí se soporten.
  • Existen restricciones a la hora de conectar ciertos tipos de puertas con ciertos tipos de qubits; el proceso de transpilación reordena el circuito y/o inserta operaciones de intercambio para solventar estas restricciones.
  • Optimización. Supone tratar de reducir el número de puertas mediante la fusión de puertas adyacentes y la eliminación de puertas innecesarias.
  • Calibraciones específicas del hardware concreto en el que se va a ejecutar el circuito.

En la segunda ejecución veremos un ejemplo de transpilar un circuito con dos niveles de profundidad para comparar los resultados. 

from qiskit_ibm_runtime import QiskitRuntimeService, Session, SamplerV2, EstimatorV2


# Save an IBM Quantum account.
QiskitRuntimeService.save_account(channel="ibm_quantum",
token="142aca5fb28d52d699c972fde878f1c3kjdue845lo3b7d2f99d52d01369195e4f5979af8dd832608ff633f475abf0eac9264d2ed3fc6e6492f7a497596e7eda3",

name="nombre_para_guardar",
set_as_default=True,
overwrite=True)
service = QiskitRuntimeService



# Cargar las credenciales guardadas
try:
service = QiskitRuntimeService(name="nombre_para_guardar")
print("Credenciales de la cuenta cargadas correctamente")
except Exception as e:
print(f"Error al cargar las credenciales: {e}")


# Optional: List all the instances you can access.
service = QiskitRuntimeService(channel='ibm_quantum')
print(f"Instancias accesibles: {service.instances()}")


# Verificar que la autenticación fue exitosa mostrando los backends disponibles
try:
backends = service.backends()
print("Backends disponibles:")
for backend in backends:
config = backend.configuration()
status = backend.status()
print(f"Backend: {backend.name}, Simulator: {config.simulator}, Operational: {status.operational}, Pending Jobs: {status.pending_jobs}")


# Filtrar los backends que no sean simuladores y que estén operativos
filtered_backends = [b for b in backends if b.status().operational]
# Verificar si hay backends disponibles después del filtrado
if not filtered_backends:
raise ValueError("No hay backends disponibles que no sean simuladores y que estén operativos.")

# Seleccionar el backend menos ocupado
backend = min(filtered_backends, key=lambda b: b.status().pending_jobs)
print(f"Seleccionado backend: {backend.name}")

# Crear una sesión
with Session(service=service, backend=backend) as session:
# Crear el Estimator y Sampler
estimator = EstimatorV2(session=session)
sampler = SamplerV2(backend=backend)
print("Sesión creada y Estimator y Sampler inicializados.")
except Exception as e:
print(f"Error durante la configuración de backends o creación de sesión: {e}")

 

 

# ESTA EJECUCIÓN NOS DA EL SIGUIENTE RESULTADO:

Credenciales de la cuenta cargadas correctamente
Instancias accesibles: ['ibm-q/open/main']
Backends disponibles:
Backend: ibm_brisbane, Simulator: False, Operational: True, Pending Jobs: 67
Backend: ibm_kyoto, Simulator: False, Operational: True, Pending Jobs: 7
Backend: ibm_osaka, Simulator: False, Operational: True, Pending Jobs: 53
Backend: ibm_sherbrooke, Simulator: False, Operational: True, Pending Jobs: 15
Seleccionado backend: ibm_kyoto
Sesión creada y Estimator y Sampler inicializados.


Nos ha indicado que hemos guardado y recuperado correctamente la información sobre nuestra
Ibm Quantum Plataform y que tenemos varias máquinas disponibles, las de Brisbane, Kyoto, Osaka y Sherbrooke
Selecciona la que está más libre, en este caso ibm_kyoto.
>>>>>>>>>>>>>>>>

# Preparamos un circuito con una puerta Hadamard y una Cnot (Bell, qubits entrelazados)

from qiskit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_histogram
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

bell = QuantumCircuit(2)
bell.h(0)
bell.cx(0, 1)
# bell.measure_all()


bell.draw(output="mpl", style="iqp")

# EL RESULTTADO ES EL CIRCUITO DE LA DERECHA

:

# Obtenemos la distribución de resultados ideal
ideal_distribution = Statevector.from_instruction(bell).probabilities_dict()
plot_histogram(ideal_distribution)

 

# EL RESULTTADO ES EL DIAGRAMADE LA DERECHA

### Ejecutamos en el BAckend elegido: ibm_kyoto

bell.measure_all()

pm = generate_preset_pass_manager(backend=backend, optimization_level=1) #esto es para compilar para la máquina backend

isa_bell = pm.run(bell) # Compilamos
job = sampler.run([isa_bell]) # lanzamos el job

# Información sobre el job
print(f">>> Job ID: {job.job_id()}")
print(f">>> Job Status: {job.status()}")

# Información sobre los resultaados
result = job.result()

# Mostrar los resultados
for idx, pub_result in enumerate(result):
print(f" > Counts for pub {idx}: {pub_result.data.meas.get_counts()}") # esto es en caso de measure_all
# print(f" > Counts for pub {idx}: {pub_result.data.c.get_counts()}") # esto es en caso de NO measure_all

# ESTA EJECUCIÓN NOS DA EL SIGUIENTE RESULTADO:

>>> Job ID: ct7zkx77rgf00085k3qg
>>> Job Status: QUEUED
 > Counts for pub 0: {'00': 2136, '01': 328, '11': 1369, '10': 263}



Nos indica que ha lanzado el Job con el ID indicado. A la finalización nos da los resultados:
El resultado ha sido '00' en 2136 ocasiones, '01' en 328, '11' en 1369 y '10'en 263

# Ahora mostraremos los resultados de forma gráfica

# Obtener las counts del resultado
# counts = pub_result.data.c.get_counts()
counts = pub_result.data.meas.get_counts() # esto es en caso de measure_all

# Convertir las claves numéricas a binario y almacenar en binary_prob
binary_prob = {k: v / sum(counts.values()) for k, v in counts.items()}

# Simular la distribución ideal
ideal_distribution = {'11': 0.5, '00': 0.5, '01': 0, '10': 0}

# Mostrar los resultados
plot_histogram([binary_prob, ideal_distribution], bar_labels=False, title="Resultados de Medida", legend=["Ejecución Real", "Ideal"])

 

# EL RESULTTADO ES EL DIAGRAMA DE ABAJO:

 

 

A continuación veremos tres imágenes del estado de la ejecución en Ibm Quantum Plataform: la primera con el Job en espera, la segunda tras su ejecución y la tercera muestra la información que nos ofrece.

# No necesitaremos conectarnos a nuestra cuenta de Ibm Quantum porque ya lo hemos hecho en el ejercicio anterior

# Create circuit to test transpiler on
from qiskit import QuantumCircuit, transpile
from qiskit.circuit.library import GroverOperator, Diagonal

# Use Statevector object to calculate the ideal output
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_histogram

# Qiskit Runtime
from qiskit_ibm_runtime import QiskitRuntimeService, Batch

# Seleccionaremos la computadora con menos trabajos encolados y mostramos su nombre
service = QiskitRuntimeService(channel="ibm_quantum")
backend = service.least_busy(operational=True, simulator=False)
backend.name

# ESTA EJECUCIÓN NOS DA EL SIGUIENTE RESULTADO:

'ibm_kyoto'

# Construimos un circuito para ver como funciona el operador de GROVER que es
# un algoritmo cuántico para la búsqueda en una base de datos no estructurada
# La primera instrucción rea una matriz diagonal con 7 "1" y un "-1" en el elemento a buscar

oracle = Diagonal([1] * 7 + [-1])
qc = QuantumCircuit(3,3)
qc.h([0, 1, 2])
qc = qc.compose(GroverOperator(oracle))

 

qc.draw(output="mpl", style="iqp")

 

# EL RESULTTADO ES EL CIRCUITO DE LA DERECHA

# Obtenemos la distribución de resultados ideal
ideal_distribution = Statevector.from_instruction(qc).probabilities_dict()
plot_histogram(ideal_distribution)

 

# EL RESULTTADO ES EL DIAGRAMADE LA DERECHA:

# Vamos a profundizar un poco con la cuestión del "Transpiler".

# las explicaciones en amarillo

 

from qiskit import transpile
from qiskit.transpiler import PassManager
from qiskit_ibm_runtime.transpiler.passes.scheduling import (
ASAPScheduleAnalysis,
PadDynamicalDecoupling,)
from qiskit.circuit.library import XGate

# Mediremos los tres qubits quedando su reultado en los bits correspondientes
qc.measure([0, 1, 2], [0, 1, 2])

# Se inicializa una lista vacía para almacenar los circuitos transpileados.
circuits = []

# Se itera sobre dos niveles de optimización: 0 y 3. El nivel 0 es sin optimización y el nivel 3 es el máximo nivel de optimización
for optimization_level in [0, 3]:
t_qc = transpile(qc, backend, optimization_level=optimization_level, seed_transpiler=0) #se transpila qc para que sea compatible con el backend
count_ops = t_qc.count_ops() # Se cuentan las operaciones en el circuito transpileado.
print (t_qc.count_ops())
if "cx" in count_ops: # Se cuentan, por ejemplo, puertas "cx"
print(f"CNOTs (optimization_level={optimization_level}): ", count_ops["cx"])
else:
print(f"CNOTs (optimization_level={optimization_level}): 0")

circuits.append(t_qc) # Se agrega el circuito transpileado a la lista circuits.

 

# Get gate durations so the transpiler knows how long each operation takes
# Obtiene la duración de las puertas para que el transpiler sepa cuanto va a tardar la ejecución
durations = backend.target.durations()


# Define una secuencia de decoupling dinámico (dos puertas X) que se aplicará a los qubits en estado de espera.
# Esto se hace para contrarrestan los efectos de los ruidos y prolongan la coherencia de los qubits en espera.
dd_sequence = [XGate(), XGate()]


# Ejecuta el PassManager en el segundo circuito transpileado (con optimización nivel 3) para aplicar el decoupling dinámico.
pm = PassManager([ASAPScheduleAnalysis(durations), PadDynamicalDecoupling(durations, dd_sequence)])
circ_dd = pm.run(circuits[1])

# Add this new circuit to our list
# Agrega el nuevo circuito con decoupling dinámico a la lista circuits.
circuits.append(circ_dd)

""" NOTA IMPORTANTE: ¿Qué hemos hecho hasta ahora?

Hemos transpilado el cirduito originar con dos niveles de optimización:
Circuitos Transpilados con Diferentes Niveles de Optimización:

Nivel de Optimización 0: Transpilación sin optimización.
Nivel de Optimización 3: Transpilación con el nivel máximo de optimización.
Se hace esto para comparar el impacto de la optimización en el circuito, especialmente en términos de la cantidad de puertas CNOT (cx),
que son costosas en términos de tiempo de ejecución y propensas a errores.

Después de obtener el circuito optimizado, hemos aplicado una secuencia de desacoplamiento dinámico para mitigar los efectos del ruido
y prolongar la coherencia de los qubits.
EL objetivo sería evaluar cómo la transpilación con diferentes niveles de optimización afecta el circuito, específicamente cantidad de operaciones cx.

¿Cuántos Circuitos se Ejecutan?
En total, se añaden tres circuitos a la lista circuits:

Circuito transpilado con nivel de optimización 0.
Circuito transpilado con nivel de optimización 3.
Circuito transpilado con nivel de optimización 3 y con desacoplamiento dinámico aplicado.

Finalmente, podemos decidir cuál o cuáles de estos circuitos ejecutar en el backend cuántico.
En muchos casos, se ejecutan todos los circuitos para comparar los resultados y elegir el mejor
enfoque para futuras implementaciones.

Por ejemplo, podríamos ejecutar los circuitos así:
# from qiskit import execute
# results = []
# for circuit in circuits:
# job = execute(circuit, backend)
# result = job.result()
# results.append(result)
#
# Analizar los resultados
# for idx, result in enumerate(results):
# counts = result.get_counts() counts = result.get_counts()
# print(f"Resultados del circuito {idx}: {counts}")
"""


""" NOTA: El tercer ejemplo trata de explicar este tipo de circuitos generados """
# Dibuja el circuito final, especificando el formato de salida (en este caso, una imagen Matplotlib),
# el estilo (iqp), y sin mostrar los cables en espera.
circ_dd.draw(output="mpl", style="iqp", idle_wires=False)

# VEMOS A CONTINUACIÓN EL DIAGRAMA DEL CIRCUITO YA TRANSPILADO:

# Ejecutamos el circuito

from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2

sampler=SamplerV2(backend)

job = sampler.run(circuits, shots=8000,)


# Información sobre el job
print(f">>> Job ID: {job.job_id()}")
print(f">>> Job Status: {job.status()}")

# Información sobre los resultaados
result = job.result()

for idx, pub_result in enumerate(result):
print(f" > Counts for pub {idx}: {pub_result.data.c.get_counts()}")
# print(f" > Counts for pub {idx}: {pub_result.data.meas.get_counts()}") # esto es en caso de measure_all

# EL RESULTADO ABAJO:

>>> Job ID: ct6q5s7wmw20008w88h0
>>> Job Status: QUEUED
 > Counts for pub 0: {'010': 773, '000': 1302, '001': 652, '110': 700, '111': 1699, '011': 1179, '101': 699, '100': 996}
 > Counts for pub 1: {'000': 546, '111': 2354, '011': 2128, '101': 1090, '001': 938, '110': 286, '010': 412, '100': 246}
 > Counts for pub 2: {'001': 948, '100': 263, '011': 1432, '111': 3533, '101': 686, '010': 449, '000': 433, '110': 256}

# Veamos gráficamente los resultados

from qiskit.visualization import plot_histogram

# Obtener las counts del resultado
counts = pub_result.data.c.get_counts()
# counts = pub_result.data.meas.get_counts() # esto es en caso de measure_all

# Convertir las claves numéricas a binario y almacenar en binary_prob
binary_prob = {k: v / sum(counts.values()) for k, v in counts.items()}

# Simular la distribución ideal
ideal_distribution = {'111': 0.781, '000': 0.031, '001': 0.031, '010': 0.031, '011': 0.031, '100': 0.031, '101': 0.031, '110': 0.031}

# Mostrar los resultados
plot_histogram([binary_prob, ideal_distribution], bar_labels=False, title="Resultados de Medida", legend=["Ejecución Real", "Ideal"])

measure_all

# EL RESULTADO ABAJO:

Interpretación de las puertas tras aplicar transpiler

Ya hemos comentado que cuando transpilamos un circuito cuántico, adaptamos sus características a las de la máquina elegida para ejecutarlo, pudiendo resultar del proceso una descomposición de las puertas cuánticas, la inclusión de otras etc… E circuito más complejo que vemos en las ejecuciones anteriores tiene varias puertas que quizás desconozcamos; vamos a ver algunas:

  • Rz:
    • Descripción: Es una rotación alrededor del eje z del qubit en la esfera de Bloch.
    • Símbolo: Generalmente se representa como Rz(θ), donde θ es el ángulo de rotación.
    • Función: Modifica la fase del estado del qubit sin cambiar su probabilidad de medir 0 o 1.
  • √X (Raíz cuadrada de X o “SX”):
    • Descripción: Representa la raíz cuadrada de la puerta Pauli-X. También conocida como la puerta de Hadamard parcial.
    • Símbolo: A veces se muestra como √X ​.
    • Función: Es un operador unitario que, cuando se aplica dos veces seguidas, equivale a una puerta X. En la esfera de Bloch, puede ser vista como una rotación de 90 grados alrededor del eje x.
  • X:
    • Descripción: Es la puerta Pauli-X, también conocida como la puerta NOT cuántica.
    • Símbolo: Se muestra como X.
    • Función: Cambia el estado de un qubit de |0⟩ a |1⟩ y viceversa. Es una rotación de 180 grados alrededor del eje x en la esfera de Bloch.
  • Delay:
    • Descripción: Representa una pausa o espera en el circuito.
    • Símbolo: A veces se muestra simplemente como "delay" o con un símbolo de pausa.
    • Función: Se usa para sincronizar operaciones en diferentes qubits o para esperar un tiempo específico antes de realizar la siguiente operación.

 

Descomposición de la Puerta Hadamard

La puerta Hadamard H puede ser descompuesta en una combinación de puertas Rz y √X (SX):

H=Rz(π/2)⋅SX⋅Rz(π/2)

Esto es debido a que la puerta Hadamard puede ser vista como una rotación específica en la esfera de Bloch. El uso de Rz y SX en lugar de H puede ser beneficioso para ciertos backends que implementan estas puertas más eficientemente.

La transpilación puede resultar en diferentes configuraciones de puertas para distintos qubits debido a optimizaciones específicas del backend cuántico, interacciones entre puertas en el circuito, y características específicas de cada qubit. Esto puede resultar en que una puerta H se descomponga en Rz y SX  en un qubit, y de manera diferente en otro, todo con el objetivo de optimizar la ejecución en el hardware cuántico específico.

Vamos a comprobar a continuación, que aplicar una puerta de Hadamard equivale a aplicar Rz·SX·Rz:

 

# Preparamos un circuito con solamente una puerta Hadamard

from qiskit import QuantumCircuit, transpile
from qiskit_aer import Aer
from qiskit.visualization import plot_bloch_multivector, plot_state_city
import matplotlib.pyplot as plt
import numpy as np
# Qiskit Runtime
from qiskit_ibm_runtime import QiskitRuntimeService, Batch

# Crear el circuito cuántico
qc = QuantumCircuit(1)
qc.h(0)
# qc.measure_all()

qc.draw(output="mpl", style="iqp")

# Ejecutamos el circuito en un simulador para visualizar el estado del qubit
# en la esfera de Bloch.

# Simulador de estado cuántico
simulator = Aer.get_backend('statevector_simulator')

# Ejecutar la simulación
result = simulator.run(transpile(qc, simulator)).result()
statevector = result.get_statevector()


print(" ")
print("Statevector resultante:")
print(statevector_array)

# Imprimir los estados base y sus correspondientes amplitudes de probabilidad
n = qc.num_qubits
for i, amplitude in enumerate(statevector_array):
binary_state = format(i, f'0{n}b')
print(f"|{binary_state}⟩: {amplitude}")
# ____________________________________________________________

# Visualizar el estado final en la esfera de Bloch
from qiskit.visualization import plot_bloch_multivector, plot_state_city
import matplotlib.pyplot as plt

plot_bloch_multivector(statevector)

 

Ahora construiremos un circuito con puertas Rz y SX para comprobar que, efectivamente, ejecutar una puerta de Hadamard es equivalente a ejecutar una combinación de las puertas que acabamos de comentar; veremos qeu el resultado en la esfera de Bloch es exactamente igual.

 

# HACEMOS AHORA UN CIRCUITO CON PUERTAS Rz, SX y Rz
#====================================================#

from qiskit import QuantumCircuit
from qiskit_aer import Aer
from qiskit.visualization import plot_bloch_multivector, plot_state_city
import matplotlib.pyplot as plt
import numpy as np
from math import pi

# Crear un nuevo circuito cuántico
qc = QuantumCircuit(1)
qc.rz(pi/2, 0)
qc.sx(0)
qc.rz(pi/2, 0)

qc.draw(output="mpl", style="iqp")

# Ejecutamos el circuito en un simulador para visualizar el estado del qubit
# en la esfera de Bloch.

# Simulador de estado cuántico
simulator = Aer.get_backend('statevector_simulator')

# Ejecutar la simulación
result = simulator.run(transpile(qc, simulator)).result()
statevector = result.get_statevector()

print(" ")
print("Statevector resultante:")
print(statevector_array)

# Imprimir los estados base y sus correspondientes amplitudes de probabilidad
n = qc.num_qubits
for i, amplitude in enumerate(statevector_array):
binary_state = format(i, f'0{n}b')
print(f"|{binary_state}⟩: {amplitude}")
# ____________________________________________________________

# Visualizar el estado final en la esfera de Bloch
from qiskit.visualization import plot_bloch_multivector, plot_state_city
import matplotlib.pyplot as plt

plot_bloch_multivector(statevector)

 

Hemos visto que RZ·SX·Rz es lo mismo que aplicar una puerta H, pero vamos a verlo paso a paso para tratar de explicarlo:

 

 

Prepararemos un circuito que solamente tenga una puerta Rz.

Al ejecutarlo veremos que no ha tenido reflejo en la esfera de Bloch; esto es porque el vector estaba exactamente en el eje "z" y la puerta Rz lo que hace es, precisamente, aplicar una rotación alrededor del eje "z" del qubit en la esfera de Bloch.

 

# Circuito con una puerta Rz

qc = QuantumCircuit(1)

qc.rz(pi/2, 0)

 

qc.draw(output="mpl", style="iqp")

 

Prepararemos ahora un circuito que tenga una puerta Rz y una SX

Al ejecutarlo veremos que se ha aplicado una rotación alrededor del eje "x" .

 

# Circuito con una puerta Rz y una SX

qc = QuantumCircuit(1)

qc.rz(pi/2, 0)

qc.sx(0)

 

qc.draw(output="mpl", style="iqp")

 

Añadiremos otra puerta Rz al circuito anterior, y comprobaremos cómo, en esta ocasión, si que la puerta Rz tiene efecto en el vector representado en la esfera de Bloch, ya que le aplica una rotación de Pi/2 alrededor del eje "x".

 

 

# Circuito con una puerta Rz, una SX y una Rz

qc = QuantumCircuit(1)

qc.rz(pi/2, 0)

qc.sx(0)

qc.rz(pi/2, 0)

 

qc.draw(output="mpl", style="iqp")

NOTA

Un estado GHZ (Greenberger-Horne-Zeilinger) es un tipo particular de estado cuántico altamente entrelazado. Es un estado que involucra múltiples qubits (generalmente tres o más) y tiene la propiedad especial de que todos los qubits están en una superposición máxima, donde cualquier medida en uno de los qubits afecta instantáneamente a los otros qubits.

El estado GHZ para 5 qubits se puede escribir de la siguiente manera:

GHZ= 1/√2 · (∣00000+∣11111⟩)

Esto significa que los cinco qubits están en una superposición de los estados ∣00000⟩ y ∣11111⟩. Si se mide uno de los qubits y se encuentra en el estado ∣0⟩|, los otros cuatro estarán en el estado ∣0⟩, y si se mide en el estado ∣1⟩, los otros cuatro también estarán en el estado ∣1⟩. Es un ejemplo de entrelazamiento, donde el estado de uno de los qubits determina el estado de los otros cuatro.

 

Paso 1: Construir el circuito GHZ

from qiskit import QuantumCircuit

def GHZ(num_qubits):

qc = QuantumCircuit(num_qubits)
qc.h(0)
for qubit in range(num_qubits-1):
    qc.cx(qubit, qubit + 1)
qc.measure_all()
return qc

num_qubits = 5

circuit = GHZ(num_qubits=num_qubits)
circuit.draw("mpl")

# ESTA EJECUCIÓN NOS DA EL SIGUIENTE RESULTADO:

Paso 2: Elegir un backend 

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

service = QiskitRuntimeService(channel="ibm_quantum")

### ELEGIR EL BACKEND MAS LIBRE DE TRABAJO
backend = service.least_busy(operational=True, simulator=False)

### ELEGIR UN BACKEND CONCRETO POR NOMBRE
# Seleccionar un backend específico por nombre
# backend_name = 'ibm_kyiv' # Cambia esto por el nombre del backend que deseas
# backend = service.get_backend(backend_name)

print(f"Backend seleccionado: {backend.name}")

print(backend)

 

# RESULTADO:

Paso 3: Transpilar el circuito para ejecución

pass_manager = generate_preset_pass_manager(optimization_level=3, backend=backend)
isa_circuit = pass_manager.run(circuit)
isa_circuit.draw("mpl", idle_wires=False, scale=0.7, fold=-1)

 

# CIRCUITO RESULTANTE ABAJO

Paso 4: Ejecultar el circuito en el SIMULADOR y en el BACKEND seleccionado

from qiskit_ibm_runtime import SamplerV2
from qiskit_aer import AerSimulator

# Ideal simulation
aer_sim = AerSimulator()
sampler_aer = SamplerV2(backend=aer_sim)
job_aer = sampler_aer.run([isa_circuit], shots=1000)

# Run on the real backend
sampler = SamplerV2(backend=backend)
job = sampler.run([isa_circuit], shots=1000)
print(f">>> Job ID: {job.job_id()}")
print(f">>> Job status: {job.status()}")

# RESULTADO:

Paso 5: Comparar ejecución en SIMULADOR y EN HARDWARE REAL

from qiskit.visualization import plot_distribution

result_aer = job_aer.result()
result = job.result()
samp_dist_aer = result_aer[0].data.meas.get_counts()
samp_dist = result[0].data.meas.get_counts()
plot_distribution([samp_dist_aer, samp_dist], legend=['Ideal', 'Real hardware'])

 

# VEMOS ABAJO EL RESULTADO

 

EXPLICACIÓN:

Podemos apreciar que la ejecución en el SIMULADOR nos ofrece el resultado esperado, cercano al 50% de posibilidades en cada resultado posible, concretamente 49% para '00000' y 51% para '11111'.

Sin embargo, la ejecución en real presenta unos resultados con errores provocados por la decoherencia de los sistemas cuánticos. Concretamente en esta ejecución los errores que deberían acercarse al 50% se quedan en 33,3% para el resultado '00000', y 39,8% para '11111', repartiéndose el resto de % hasta el 100% en algunos de los diferentes valores de entre los 32 posibles.
Se aprecia un salto significativo (15,7%) en el valor '00010', para el que no encontramos explicación; de hecho, en una nueva ejecución los resultados obtenidos han sido más normales.
INCLUIMOS A CONTINUACIÓN el gráfico de esta nueva ejecución, pero hemos decidido dejar también la anterior para mostrar que no siempre los resultados son los esperados.

 

 

Diseña y Ejecuta con IBM Quantum Composer

Veremos en esta sección como trabajar con el Quantum Composer de IBM.

Se trata de una herramienta que nos permite crear y ejecutar circuitos cuánticos sin necesidad de instalar nada en nuestro equipo.

Podemos acceder a ella a través de este enlace y podemos encontrar toda la documentación aquí.

Índice:

  1. ¿Qué es Composer?
  2. Construir un circuito
  3. Inspeccionar paso a paso e interpretar la representación del estado de los qubit
  4. Visualizaciones (probabilidades, q-esfera y statevector)
  5. Composer Operations
  6. Ejecutar circuitos

IBM Quantum Composer es una utilidad de IBM Platform que facilita crear, visualizar y ejecutar circuitos cuánticos en simuladores o en sistemas cuánticos; una vez construidos los circuitos, permite:

  • Visualizar el estado de los qubits

Para cada qubit se muestra una esfera interactiva en la que se ven los cambios que el circuito va operando en el estado de los qubits.

  • Ejecutar en hardware cuántico

Ejecutar los circuitos en hardware cuántico real para comprender los efectos del ruido en el proceso.

  • Generar código automáticamente para utilizarlo en otros entornos

Según se va creando el circuito se irá generando el código correspondiente en OpenQASM o Python

Vemos a continuación una imagen de la pantalla de Composer y comentaremos sus partes más interesantes:

  1. Circuit Name edit
  2. Menu bar- Utilice estos menús para crear un nuevo circuito, administrar circuitos y registros guardados, personalizar su espacio de trabajo y más.
  3. Run area– Para ejecutar el circuito en un simulador o en hardware real.
  4. Tools panels– Tres iconos para ver los circuitos que tenemos guardados, los job’s ejecutados y la documentación de ayuda. Pinchando de nuevo en el icono se cierra el panel.
  5. Operations catalog- Estos son los componentes básicos de los circuitos cuánticos. Arrastre y suelte estas puertas y otras operaciones en el editor gráfico de circuitos. Los diferentes tipos de puertas se agrupan por color. Por ejemplo, las puertas clásicas son de color azul oscuro, las puertas de fase son de color azul claro y las operaciones no unitarias son de color gris.
  6. Inspect– Desde aquí podremos ejecutar el circuito paso a paso (consultar Inspect your circuit, step-by-step).
  7. Phase disks– Representación de la fase del vector de estado del qubit y de su grado de entrelazamiento.
  8. Code editor– Desde aquí podemos ver y editar directamente el código OpenQASM o Qiskit.
  9. Graphical circuit editor– Aquí es donde vamos construyendo el circuito. Arrastraremos puertas y otras operaciones sobre los “cables” de qubit horizontales que conforman el circuito cuántico. Para editar los parámetros y configuraciones de las puertas, seleccione la puerta en el editor gráfico y haga clic en Editar.
  10. Visualizations– Más adelante se explica las tres visualizaciones: Probabilidades, Q-Esfera y Statevector

Probemos con un circuito sencillo, concretamente un estado de Bell de dos qubits (máximo entrelazamiento).

Para ello arrastraremos sobre el primer qubit (q0) una puerta de Hadamard:

 

En la zona de visualización tendremos lo siguiente:

Es el resultado lógico: el qubit 1 está siempre a 0 y el qubit 1 está a un 50% de probabilidades de estar en 0 o en 1. Más adelante se explican los otros modos de visualización.

A continuación, entrelazaremos los dos qubits a través de una CNOT entre q0 y q1

Nuestra visualización habrá cambiado porque ahora las únicas posibilidades es que los dos qubits estén a 0 y que los dos qubits estén a 1

Para acabar podemos incluir mediciones:

 

A la derecha podemos ver el código generado y podemos

exportarlo para utilizarlo en otras aplicaciones:

Inspeccionar paso a paso el circuito

  • En el menú “Ver” seleccionamos los paneles para las visualizaciones que queramos utilizar.
  • Hacemos clic en el interruptor Inspeccionarde la barra de herramientas. Hay que tener en cuenta que una vez que el Inspector de circuitos esté activado, no podremos agregar más operaciones hasta que lo desactivemos.
  • Para movernos paso a paso a través de las visualizaciones de los componentes del circuito podemos utilizar los botones de avance y retroceso.
  • Para inspeccionar solo alguna/s operación/es, podemos hacer clic en dichas operaciones y aparecerá una superposición que indica que se incluirán cuando ejecute el Inspector de circuitos. Para anular la selección de una operación, haga clic en ella nuevamente y la superposición desaparecerá.

Para cerrar el Inspector y volver a editar el circuito, hay que hacer de nuevo clic en el botón Inspeccionar en la barra de herramientas.

 

Representación del estado de los qubit

El disco que vemos al final de la línea que representa el qubit en IBM Quantum Composer proporciona el estado local de cada qubit al final del cálculo. Esta es la triple información que nos da:

1. Probabilidad de que el qubit esté en el estado∣1⟩ 

La probabilidad de que el qubit esté en el estado ∣1⟩ está representado por el relleno del disco azul:

 

2. Fase cuántica

La probabilidad de que el qubit esté en el estado ∣1⟩ está representado por el relleno del disco azul:

 

3. Pureza/entrelazamiento del Qubit

El radio del anillo negro representa la pureza reducida del estado del qubit. La pureza está en el rango [0, 5.1]; un valor de uno indica que el qubit no está entrelazado con ninguna otra parte. Por el contrario, una pureza reducida de 0,5 muestra que el qubit queda en un estado completamente mixto y tiene cierto nivel de entrelazamiento sobre el resto de N−1 qubits, y posiblemente incluso el entorno.

 

Estas son los tres tipos de visualizaciones que nos ofrece Composer:

1. Vista de Probabuilidades

(Máximo 8 qubits)

 

2. Vista de Q-Esfera

(Máximo 5 qubits)

Los nodos se disponen en la esfera q de modo que el estado base con todos los ceros ∣000⟩ está en su polo norte, y el estado base con todos unos ∣111⟩ está en su polo sur. Los estados base con el mismo número de ceros (o unos) se encuentran en una latitud compartida de la esfera q (por ejemplo, ∣001⟩, ∣010⟩, ∣100⟩, ∣001 ⟩, ∣010 ⟩, ∣100 ⟩). Comenzando en el polo norte de la esfera q y progresando hacia el sur, cada latitud sucesiva tiene estados base con un mayor número de unos; la latitud de un estado base está determinada por su Distancia de Hamming Desde el estado cero. La esfera q contiene información completa sobre el estado cuántico en una representación compacta.

 

Nota:

¿Cuál es la diferencia entre una esfera de Bloch y una Q-Esfera? Es importante destacar que la q-Esfera no es lo mismo que la esfera de Bloch, incluso para un solo qubit. La esfera de Bloch ofrece una visión local del estado cuántico, donde cada qubit se ve por sí solo. Cuando se intenta comprender cómo se comportan los registros de qubits (estados multiqubit) en la aplicación de circuitos cuánticos, resulta más esclarecedor adoptar una visión global y observar el estado cuántico en su totalidad. La Q-Esfera proporciona una representación visual del estado cuántico y, por lo tanto, este punto de vista global . Por lo tanto, al explorar aplicaciones y algoritmos cuánticos en pequeñas cantidades de qubits, la Q-Esfera debería ser el método de visualización principal.

3. Vista de Statevector

(Máximo 6 qubits)

Esta vista visualiza las amplitudes cuánticas como un gráfico de barras. El eje horizontal etiqueta los estados de base computacional. El eje vertical mide la magnitud de las amplitudes asociado con cada estado de base computacional. El color de cada barra representa la fase cuántica.

 

Si detenemos el cursor sobre alguna de las columnas, veremos la información correspondiente:

Puedes descargar un PDF con la relación de operadores en este enlace.

Ejecutar circuitos

  • Hacer clic en “Setup and run”en la esquina superior derecha. En la ventana que se abre, seleccionar un sistema o simulador disponible. Para ello será interesante atender a la cantidad de trabajos que hay encolados en cada máquina: “Total pending Jobs
  • A continuación, podemos establecer el número de “shots” (ejecuciones) que queremos realizar con nuestro circuito.
  • También podemos agregar etiquetas en este panel.
  • Hacer Clic en “Run in …..
  • Podremos ver el progreso del trabajo haciendo clic en el ícono Trabajos:
linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram