Saltar al contenido principal

Simular un modelo de Ising pateado con la función TEM

El método de mitigación de errores mediante redes tensoriales (TEM) de Algorithmiq es un algoritmo híbrido cuántico-clásico diseñado para realizar la mitigación del ruido íntegramente en la etapa de post-procesamiento clásico. Con TEM, puedes calcular los valores esperados de los observables mitigando los inevitables errores inducidos por el ruido en el hardware cuántico con mayor precisión y eficiencia de coste, lo que lo convierte en una opción muy atractiva para investigadores cuánticos y profesionales de la industria.

Este tutorial demuestra cómo TEM puede obtener resultados significativos para la dinámica de un sistema cuántico, que sería inaccesible sin mitigación de errores y que requiere sustancialmente más recursos cuánticos si se utilizan otros métodos de mitigación de errores como PEC y ZNE.

Estimación de uso: este notebook utiliza aproximadamente 10 minutos de QPU en dispositivos Heron r3. El tiempo de ejecución puede variar considerablemente según el dispositivo elegido. Las estimaciones de uso por sección se encuentran a continuación.

Ejecutar experimentos de física de muchos cuerpos con mitigación de errores mediante la función TEM

Este tutorial se basa en la siguiente referencia: L. E. Fischer et al., Nat. Phys. (2026). Esta referencia analiza una simulación real en hardware cuántico de hasta 91 qubits. En este tutorial, recreamos una simulación similar en un circuito de menor tamaño.

El modelo de Ising pateado corresponde al modelo de Ising habitual:

H^I=Jn=0N2Z^nZ^n+1+hn=0N1Z^n\hat{H}_{\text{I}} = J \sum_{n=0}^{N-2} \hat{Z}_n \hat{Z}_{n+1} + h \sum_{n=0}^{N-1} \hat{Z}_n

al que se le aplica un pateo transversal:

H^K=bn=0N1X^n\hat{H}_{K} = b \sum_{n=0}^{N-1} \hat{X}_n

El objetivo es simular la dinámica de un estado bajo el Hamiltoniano de Ising pateado transversal, cuya evolución temporal puede implementarse mediante un unitario de Floquet U^KI=eiH^KeiH^I\hat{U}_{\text{KI}} = e^{-i \hat{H}_K} e^{-i \hat{H}_I} . El estado inicial a evolucionar es aquel en el que el primer qubit está en el estado +|+\rangle, mientras que los demás están emparejados y en el estado de Bell (00+11)/2(|00\rangle + |11\rangle)/\sqrt{2}.

La cantidad que queremos observar es la función de correlación. El artículo de referencia discute cómo esta cantidad puede reescribirse como un operador de Pauli X^\hat{X} en el neˊsimon^{ésimo} qubit. Después de un número de pasos de tiempo físicos tt, calculamos el valor del operador de Pauli X^n=t\hat{X}_{n=t}. Dependiendo de los parámetros del sistema, el valor de este observable es igual a un valor que puede calcularse exactamente, o solo simularse mediante métodos aproximados. Específicamente, para J=b=π/4|J|=|b|=\pi/4, es igual a [cos(2h)]t[\cos(2h)]^t, que es el valor que usaremos para validar los resultados de este tutorial. Además, en un paso de tiempo tt dado, X^nt\langle\hat{X}_{n\neq t}\rangle es cero. Para obtener detalles sobre cómo obtener estos valores y para comparar con resultados de simulación clásica aproximada fuera de estos parámetros, consulta L. E. Fischer et al., Nat. Phys. (2026).

TEM funciona caracterizando primero el ruido de cada capa única de gates de dos qubits en el circuito, así como caracterizando el error de lectura. Luego, el circuito se ejecuta en la máquina cuántica. Finalmente, la mitigación de errores mediante red tensorial se realiza en los recursos clásicos en IBM Cloud® y se devuelve el valor mitigado. En este ejemplo, el circuito tiene dos capas únicas para caracterizar.

Configuración

Como requisito previo, asegúrate de que las dependencias necesarias estén instaladas.

%pip install numpy matplotlib qiskit qiskit-ibm-catalog qiskit-ibm-runtime pylatexenc qiskit_qasm3_import
import os
from matplotlib import pyplot as plt
import numpy as np

from qiskit.quantum_info import SparsePauliOp
from qiskit.qasm3 import load

from qiskit_ibm_catalog import QiskitFunctionsCatalog

Mitigación de errores con TEM

Proporcionamos aquí un circuito que implementa el modelo de Ising pateado descrito anteriormente. El circuito se prepara de la siguiente manera. Primero, hay una fase de preparación del estado, en la que el primer qubit está en el estado +|+\rangle, mientras que los demás están en pares de Bell (00+11)/2(|00\rangle + |11\rangle)/\sqrt{2}. Esto va seguido de la estructura en mosaico que implementa la evolución unitaria U^KI\hat{U}_{\text{KI}}. El número de pasos de tiempo físicos corresponde a t/2t/2 capas de circuito. El siguiente código descarga los dos archivos QASM necesarios para este tutorial.

# Download required QASM files
import urllib

urllib.request.urlretrieve(
"https://ibm.box.com/shared/static/swy5jtq309b0xpzluzlmsmj908yphes8.qasm",
"ki_30q.qasm",
)
urllib.request.urlretrieve(
"https://ibm.box.com/shared/static/et3gkodonw6gsp2trs43lzaozrdtiu7s.qasm",
"ki_12q.qasm",
)

Podemos visualizar una versión pequeña del circuito, con 12 qubits y seis pasos de tiempo:

# Parameters of the kicked Ising model
h = 0.0
num_qubits = 12
t_steps = 6

# Load the circuit for the kicked Ising model
small_circuit = load("ki_12q.qasm")

# Draw the circuit
small_circuit.draw("mpl", scale=0.25, fold=-1)

Output of the previous code cell

A continuación, construye el observable X^n=t\hat{X}_{n=t}. Se construye como una cadena de Pauli simple con el orden que coincide con el utilizado por Qiskit:

def xt_observable(n_qubits, t_steps):
pauli_str = "".join(["I" * t_steps, "X", "I" * (n_qubits - t_steps - 1)])
pauli_str = pauli_str[::-1] # Reverse the string to match qiskit order
return SparsePauliOp(data=pauli_str, coeffs=1.0)

En nuestro pequeño ejemplo de 12 qubits, el observable se ve así:

# Build the observable for the kicked Ising model
small_observable = xt_observable(n_qubits=12, t_steps=6)
print(small_observable)
SparsePauliOp(['IIIIIXIIIIII'],
coeffs=[1.+0.j])

Las funciones de Qiskit utilizan PUBs como medio para recopilar las entradas. En nuestro caso, consideremos un único circuito y observable como nuestro PUB:

# Collect the input PUBs, in this case composed of a
# single circuit and observable
pubs = [(small_circuit, [small_observable])]

A continuación, obtenemos acceso a la función TEM. Primero configuramos la autenticación requerida para IBM Cloud y seleccionamos un backend de los dispositivos disponibles. El token, los backends disponibles y los nombres de recursos de nube correspondientes (CRN) pueden obtenerse iniciando sesión en tu cuenta en el panel de control de IBM Quantum Platform.

# Set IBM Quantum credentials and backend configuration
personal_token = os.environ.get(
"QISKIT_IBM_TOKEN", "<API-KEY>"
) # Replace with your personal token or set the environment variable
channel = "ibm_quantum_platform"
crn = "your_crn" # Replace with the Cloud Resource Name (CRN)

# Select the QPU backend
backend_name = "ibm_qpu_name" # Replace with your desired backend's name

Carga la función TEM desde el Qiskit Functions Catalog:

# Load the TEM function from the Qiskit Functions Catalog
catalog = QiskitFunctionsCatalog(
channel=channel,
token=personal_token,
instance=crn,
)
tem = catalog.load("algorithmiq/tem")

Ahora podemos ejecutar un experimento en el circuito de Ising pateado con la mitigación de errores proporcionada por TEM. Con la configuración predeterminada, TEM puede ejecutarse de manera sencilla con un tiempo de ejecución QPU esperado de alrededor de 2,5 minutos, dependiendo del QPU:

tem_job = tem.run(pubs=pubs, backend_name=backend_name)

Con las opciones predeterminadas, la función TEM ejecuta tres jobs en el ordenador cuántico: aprendizaje del ruido, mitigación de lectura y muestreo del circuito. El número de shots utilizados por cada uno de estos puede modificarse en las opciones pasadas a la función. Por defecto, estos parámetros están configurados para lograr una precisión de 0,05 en los valores esperados mitigados. Puedes verificar el estado de tu job en el panel de control de IBM Quantum Platform o con:

print(tem_job.status())
QUEUED

Cuando el estado sea DONE, podemos verificar los resultados brutos y mitigados. Los tem_evs definidos a continuación son los valores esperados de los observables solicitados, en este caso un solo observable, X^n=t\langle \hat X_{n=t}\rangle, y tem_std son las desviaciones estándar correspondientes.

# Get the results of the TEM job
tem_results = tem_job.result()[
0
] # Get the first and only result from the job
tem_evs = tem_results.data.evs[0]
tem_std = tem_results.data.stds[0]

print(f"TEM Result: {tem_evs:.3f} ± {tem_std:.3f}")
TEM Result: 1.031 ± 0.046

También podemos verificar cuánto tiempo de ejecución cuántica se utilizó para cada llamada en IBM Quantum Platform, o inspeccionando los metadatos de resultados desde el código Python.

# Get the TEM job runtime
tem_runtime = tem_job.result().metadata["resource_usage"][
"RUNNING: EXECUTING_QPU"
]["QPU_TIME"]

print(f"TEM Runtime: {tem_runtime} seconds")
TEM Runtime: 155.0 seconds

Personalizar los parámetros TEM y opciones avanzadas

La función TEM proporciona varias opciones avanzadas para personalizar tu flujo de trabajo de mitigación de errores. Estas opciones te permiten controlar la precisión, el número de shots, las estrategias de aprendizaje del ruido y otros parámetros para adaptarse mejor a los requisitos de tu experimento y a los recursos cuánticos disponibles.

Las opciones avanzadas comunes son:

  • precision: Especifica la precisión objetivo para los valores esperados mitigados.
  • default_shots: En lugar de precision, puedes especificar el número de shots utilizados por el job de medición.
  • tem_max_bond_dimension: La dimensión de enlace máxima utilizada en la red tensorial.
  • tem_compression_cutoff: El valor de corte a usar para la red tensorial.
  • opciones de aprendizaje del ruido: Configura cómo se caracteriza el ruido, como el número de repeticiones o circuitos de calibración específicos.
  • private: Asegura que los circuitos y los resultados del experimento sean privados para ti y deshabilita las descargas múltiples de los resultados del job.

Consulta la documentación TEM o el Qiskit Functions Catalog para obtener una lista completa de opciones compatibles y sus descripciones. Puedes ajustar estos parámetros para equilibrar el tiempo de ejecución, el uso de recursos y la precisión de los resultados. Puedes pasar estas opciones como un diccionario al argumento options al ejecutar la función TEM:

options = {
"default_shots": 10_000,
"tem_max_bond_dimension": 512,
"tem_compression_cutoff": 1e-16,
# This option helps optimizing the measurement
# stage since the observable is strongly biased
# toward the X operator for all the qubits.
"compute_shadows_bias_from_observable": True,
# set to True to keep experiment results private,
# recommended for confidential circuits
"private": False,
}

También se pueden pasar opciones personalizadas para el aprendiz de ruido. Siguen las definiciones utilizadas en el Runtime Qiskit NoiseLearnerOptions:

nl_options = {
"num_randomizations": 32,
"max_layers_to_learn": 2,
"shots_per_randomization": 128,
"layer_pair_depths": [0, 1, 2, 4, 16, 32],
}

# add noise learning options to the overall options
options |= nl_options

Vuelve a ejecutar el experimento con estas opciones personalizadas ajustadas a nuestro circuito. El tiempo de ejecución esperado es de aproximadamente cuatro minutos de QPU.

tem_job_custom = tem.run(
pubs=pubs, backend_name=backend_name, options=options
)

Si el job no está configurado como privado, podemos recuperar el resultado en un momento posterior. Para ello, guarda el ID del job impreso aquí y usa tem_job_custom = catalog.get_job_by_id("your-job-id").

job_id = tem_job_custom.job_id
print(f"Job ID: {job_id}")
Job ID: 1ba10094-a541-457a-9287-dbd49306d12d
results_custom = tem_job_custom.result()
tem_evs = results_custom[0].data.evs[0]
tem_std = results_custom[0].data.stds[0]

print(f"TEM Result: {tem_evs:.3f} ± {tem_std:.3f}")
TEM Result: 0.956 ± 0.018

Ahora podemos inspeccionar los resultados y los metadatos para obtener información sobre el experimento:

metadata_custom = results_custom[0].metadata

unmitigated_evs = metadata_custom["evs_non_mitigated"][0]
unmitigated_stds = metadata_custom["stds_non_mitigated"][0]
print(f"Unmitigated Result: {unmitigated_evs:.3f} ± {unmitigated_stds:.3f}")

# Exact result for the kicked Ising model from the reference paper
exact_evs = np.cos(2 * h) ** t_steps
print("Exact Result:", exact_evs)
Unmitigated Result: 0.894 ± 0.015
Exact Result: 1.0
# Plot comparing the different expectation values
plt.bar(
["Unmitigated", "TEM"],
[unmitigated_evs, tem_evs],
yerr=[unmitigated_stds, tem_std],
color=["grey", "c"],
)
plt.hlines(y=exact_evs, xmin=-0.5, xmax=1.5, colors="r", linestyles="dashed")
plt.ylabel("Expectation Value")
plt.ylim(0, 1.1)
plt.show()

Output of the previous code cell

Finalmente, podemos verificar el impacto de las opciones personalizadas en el tiempo de ejecución QPU y clásico:

# Get the metadata of the TEM job
job_metadata = results_custom.metadata

# Get the runtime of the TEM job
qpu_runtime = job_metadata["resource_usage"]["RUNNING: EXECUTING_QPU"][
"QPU_TIME"
]
classical_runtime = (
job_metadata["resource_usage"]["RUNNING: OPTIMIZING_FOR_HARDWARE"][
"CPU_TIME"
]
+ job_metadata["resource_usage"]["RUNNING: POST_PROCESSING"]["CPU_TIME"]
)

print(f"QPU Runtime: {qpu_runtime} seconds")
print(f"Classical Runtime: {classical_runtime} seconds")
QPU Runtime: 342.0 seconds
Classical Runtime: 107.632604 seconds

Escalar TEM a circuitos grandes

Los circuitos grandes pueden, en principio, ejecutarse con la función TEM. Sin embargo, es importante tener en cuenta las limitaciones de los recursos clásicos, ya que TEM se ejecuta en runners de IBM Cloud con tiempos de ejecución potencialmente muy largos. Para circuitos extremadamente grandes, contacta al equipo de soporte de TEM en qiskit_ibm@algorithmiq.fi.

Aquí ejecutamos un ejemplo con un circuito más grande de 30 qubits a escala de utilidad, optimizando los parámetros TEM para la velocidad en lugar de la precisión.

# Kicked Ising model parameters
n_qubits = 30
t_steps = 15
h = 0.0

# Load the circuit for the kicked Ising model
circuit = load("ki_30q.qasm")

# Build the observable for the kicked Ising model
observable = xt_observable(n_qubits=n_qubits, t_steps=t_steps)

# Collect the input PUBs, in this case composed of a
# single circuit and observable
pubs = [(circuit, [observable])]

Definamos algunas opciones orientadas al rendimiento:

options = {
"num_randomizations": 32,
"max_layers_to_learn": 2,
"shots_per_randomization": 128,
"layer_pair_depths": [0, 1, 2, 4, 16, 32, 64],
"default_shots": 5_000,
"tem_max_bond_dimension": 128,
"tem_compression_cutoff": 1e-10,
"compute_shadows_bias_from_observable": True,
"private": False,
}

Finalmente, ejecuta el experimento, obtén el resultado y visualízalo. Esto tardará aproximadamente 3,5 minutos de QPU.

tem_job_large = tem.run(pubs=pubs, backend_name=backend_name, options=options)
job_id = tem_job_large.job_id
print(f"Job ID: {job_id}")
Job ID: 9f3f190f-f4b0-4dcb-bb83-5f71f37d0d77
results_large = tem_job_large.result()
tem_evs = results_large[0].data.evs[0]
tem_std = results_large[0].data.stds[0]

print(f"TEM Result: {tem_evs:.3f} ± {tem_std:.3f}")

# Get the metadata of the TEM job
job_metadata = tem_job_large.result().metadata

# Get the runtime of the TEM job
qpu_runtime = job_metadata["resource_usage"]["RUNNING: EXECUTING_QPU"][
"QPU_TIME"
]
classical_runtime = (
job_metadata["resource_usage"]["RUNNING: OPTIMIZING_FOR_HARDWARE"][
"CPU_TIME"
]
+ job_metadata["resource_usage"]["RUNNING: POST_PROCESSING"]["CPU_TIME"]
)

print(f"QPU Runtime: {qpu_runtime} seconds")
print(f"Classical Runtime: {classical_runtime} seconds")
TEM Result: 0.794 ± 0.026
QPU Runtime: 203.0 seconds
Classical Runtime: 251.71805499999996 seconds
# Plot comparing the different expectation values
metadata_large = results_large[0].metadata
unmitigated_evs = metadata_large["evs_non_mitigated"][0]
unmitigated_stds = metadata_large["stds_non_mitigated"][0]

exact_evs = np.cos(2 * h) ** t_steps

plt.bar(
["Unmitigated", "TEM"],
[unmitigated_evs, tem_evs],
yerr=[unmitigated_stds, tem_std],
color=["grey", "c"],
)
plt.hlines(y=exact_evs, xmin=-0.5, xmax=1.5, colors="r", linestyles="dashed")
plt.ylabel("Expectation Value")
plt.ylim(0, 1.1)
plt.show()

Output of the previous code cell