Saltar al contenido principal

Despliega y ejecuta una plantilla de Qiskit Function para dinámica hamiltoniana AQC + Trotter

Descripción general

Esta es una plantilla de Qiskit Function agnóstica al experimento para dinámica hamiltoniana. Dado un hamiltoniano de Pauli 1D de vecino más cercano, un estado inicial preparado (opcional), y un conjunto de observables, ejecuta la evolución temporal de Trotter, la compresión de circuitos por compilación cuántica aproximada (AQC), y la ejecución mitigada, y luego devuelve la serie temporal de cada observable. Intercambia la configuración (PRE) y el análisis (POST) y el mismo núcleo impulsa un experimento diferente:

PRE (tu configuración)FUNCTION (desplegado aquí)POST (tu análisis)
Prepara un estado, como un circuito o un estado producto, con un impulso local opcionalSíntesis de Trotter → compresión AQC → ejecución en statevector, fake, o runtime, devolviendo O(t)\langle O \rangle(t)S(q,ω)S(q, \omega) para dispersión de neutrones, o magnetización, transporte, dinámica de quench, y así sucesivamente

La plantilla está publicada en el repositorio de plantillas de Qiskit Function, junto con las otras plantillas de aplicación. Este cuaderno la despliega en tu propia cuenta de Qiskit Serverless. Ejecútalo una vez, y cualquier cuaderno podrá luego llamar a la función con serverless.load("aqc-dynamics-function").

Para ver un ejemplo científico completo, consulta Simular dispersión de neutrones con un flujo de trabajo Serverless de dinámica AQC + Trotter, que llama a esta función para calcular el factor de estructura dinámica de KCuF3_3. Este cuaderno cubre el despliegue y el contrato de entrada en su lugar.

Requisitos

Antes de empezar, asegúrate de tener lo siguiente en el entorno del kernel de este cuaderno:

  • Qiskit SDK v2.0 o posterior (pip install qiskit).

  • El cliente de Qiskit IBM Catalog (pip install qiskit-ibm-catalog), que despliega y ejecuta cargas de trabajo en Qiskit Serverless.

Las dependencias científicas propias de la función (qiskit-addon-aqc-tensor, cotengrust, qiskit-aer) no necesitan instalarse localmente.

Obtener los archivos fuente de la plantilla

La función es un pequeño paquete de Python que Qiskit Serverless ejecuta en la nube, por lo que su fuente debe existir como archivos locales que se suben en el momento del despliegue. El paquete está publicado en el repositorio de plantillas de Qiskit Function.

Descarga source_files

La descarga es un único zip, nombrado según la ruta completa del directorio en el repositorio:

qiskit-community qiskit-function-templates main physics aqc_trotter source_files.zip

  1. Descomprímelo en el directorio que contiene este cuaderno.

  2. Renombra la carpeta extraída de ese nombre largo a source_files.

Tu directorio de trabajo entonces se verá así:

your-working-directory/
├── function-template-aqc-trotter.ipynb <- this notebook
└── source_files/ <- the renamed folder
├── __init__.py
├── program.py
└── source/
├── __init__.py
├── _serverless.py
├── app_function.py
├── aqc.py
├── build.py
├── execute.py
└── hamiltonian.py

El nombre debe ser exactamente source_files, porque ese es el working_dir que el Paso 3 sube.

program.py es el punto de entrada que invoca el gateway. Todo lo que está bajo source/ es la implementación, dividida por etapa: síntesis hamiltoniana y de Trotter, compresión AQC, y ejecución. Nada de esto necesita edición para ejecutar los ejemplos que siguen. El Paso 3 sube todo el directorio, así que repite ese paso cada vez que cambies un archivo.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-ibm-catalog

1. Autenticación

Usa qiskit-ibm-catalog para autenticarte con QiskitServerless usando tu clave API (token) y CRN (instancia), que puedes encontrar en el panel de IBM Quantum® Platform. Con estas credenciales puedes instanciar el cliente serverless localmente para subir o ejecutar la función seleccionada:

from qiskit_ibm_catalog import QiskitServerless
serverless = QiskitServerless(channel="ibm_quantum_platform", token="MY_TOKEN", instance="MY_CRN")

Opcionalmente puedes usar save_account() para guardar tus credenciales en tu entorno local (consulta la guía Configura tu cuenta de IBM Cloud®). Ten en cuenta que esto escribe tus credenciales en el mismo archivo que QiskitRuntimeService.save_account():

QiskitServerless.save_account(channel="ibm_quantum_platform", token="MY_TOKEN", instance="MY_CRN")

Si la cuenta está guardada, no hay necesidad de proporcionar el token para autenticarte:

from qiskit_ibm_catalog import QiskitServerless

# Authenticate to the remote cluster
# In this case, loading a saved account
serverless = QiskitServerless()

# REPLACE WITH YOUR OWN CREDENTIALS or SAVED ACCOUNT
# serverless = QiskitServerless(channel="ibm_quantum_platform", token="MY_TOKEN", instance="MY_CRN")

2. Declarar dependencias

Paquetes que la función necesita además de la imagen base gestionada de serverless.

nota

El gateway solo instala nombres en su lista de permitidos (requirements-dynamic-dependencies.txt), coincidiendo por nombre de paquete y fijado a la versión permitida con ==. Cualquier otra cosa debe llegar de forma transitiva (como dependencia de un paquete permitido). La sintaxis [extras] se respeta: qiskit-addon-aqc-tensor[quimb-jax] es lo que instala quimb y jax. cotengrust es necesario para la eficiencia de memoria durante la simulación de redes tensoriales. qiskit-aer se lista por separado para el backend fake (simulación ruidosa local).

DEPENDENCIES = [
"qiskit-addon-aqc-tensor[quimb-jax]==0.3.1",
"qiskit-aer==0.17.2",
"cotengrust==0.2.0",
]

3. Definir y subir la función

from qiskit_ibm_catalog import QiskitFunction

fn = QiskitFunction(
title="aqc-dynamics-function",
entrypoint="program.py",
working_dir="source_files/",
dependencies=DEPENDENCIES,
)
serverless.upload(fn)
QiskitFunction(aqc-dynamics-function)

4. Verificar que se registró

next(p for p in serverless.list() if p.title == "aqc-dynamics-function")
QiskitFunction(aqc-dynamics-function)

Referencia de la función

Esta es una breve introducción. Cada campo está documentado en su totalidad en el README de la plantilla AQC Dynamics: la tabla completa de entradas con sus reglas de validación, los campos de salida, los backends de ejecución, y más ejemplos completos. Lo que sigue es la versión corta, suficiente para leer los ejemplos que continúan.

Entradas

Cada ejecución es una única llamada a fn.run(...). Solo las tres primeras entradas de la tabla son obligatorias: hamiltonian, t_steps, y aqc_segments. Todo lo que viene después es opcional y recurre al valor predeterminado mostrado, por lo que una llamada mínima consta de tres argumentos y el resto de la tabla es la funcionalidad a la que puedes optar. El num_qubits del hamiltoniano establece la longitud de la cadena, por lo que no hay una entrada de tamaño separada.

EntradaPredeterminadoDescripción
hamiltonianobligatorioHamiltoniano de Pauli 1D de vecino más cercano como un SparsePauliOp. Las cadenas son operadores de Pauli, por lo que no hay un factor implícito de un medio.
t_stepsobligatorioPasos totales de Trotter. Evoluciona hasta T = t_steps * dt y reporta cada observable en cada t_k = k * dt.
aqc_segmentsobligatorioPlan de compresión: una lista de {"n_steps": k, "ansatz_steps": m}. Se comprimen sum(n_steps) pasos; el resto se ejecuta como Trotter simple.
dt0.2Tiempo físico avanzado por un paso de Trotter.
initial_state|0...0>Un QuantumCircuit preparado para evolucionar. Incorpora cualquier impulso local en este circuito.
observablesZ por sitioCualquier cosa que EstimatorV2 acepte como argumento observables. Un observable por columna de salida.
trotter_optionsSuzuki de 2do orden{"method": ..., "synthesis_settings": {...}}. reps y time son propiedad de la función.
aqc_optionsver descripciónmax_bond (32), cutoff (1e-8), autodiff_backend ("jax"), fidelity_target (None), optimizer_settings (L-BFGS-B, jac=True, maxiter=300).
estimator_optionsDD, twirling, TREXEstimatorV2.options, pasado tal cual. Un diccionario proporcionado reemplaza los valores predeterminados por completo en lugar de fusionarse con ellos.
transpiler_options{"optimization_level": 3}Argumentos de palabra clave de generate_preset_pass_manager. backend y target se rechazan, ya que la ruta de ejecución los posee.
backend"runtime""statevector", "fake", o "runtime".
backend_namemenos ocupadoNombre de backend IBM® para runtime, o un backend falso con nombre.
batches1Divide los circuitos entre N trabajos de runtime. Un lote envía un único trabajo y no crea sesión.
parallel_simFalseDistribuye las rutas del simulador local entre todos los núcleos disponibles con Ray. Sin efecto en runtime.
return_circuitsFalseDevuelve los circuitos lógicos AQC + Trotter en el resultado junto con la serie de observables.

Backends de ejecución

Las tres rutas comparten el mismo código y la misma configuración de mitigación. Solo difieren en dónde se ejecutan los circuitos.

backendQué esCredencialesNotas
"statevector"StatevectorEstimator exactoSolo cuenta ServerlessLa ruta de referencia exacta. Sin tiempo de QPU.
"fake"Simulación local ruidosa en un backend falso de QiskitSolo cuenta ServerlessUn ensayo fiel de la ruta mitigada de runtime. Necesita qiskit-aer. Por defecto usa el fake_sherbrooke de 127 qubits.
"runtime" (predeterminado)El EstimatorV2 mitigado contra un QPU realCuenta Serverless y una instancia con acceso a QPUbackend_name opcional; si se omite, selecciona el dispositivo menos ocupado.

Ambas rutas de simulador siguen llamando a la función desplegada, por lo que necesitan una cuenta Serverless guardada aunque no usen tiempo de QPU. Los dos ejemplos que siguen ejecutan la misma carga de trabajo primero en statevector, luego en runtime.

Salida

job.result() devuelve un diccionario simple:

{
"times": [...], # length t_steps + 1, t_k = k * dt (t=0 is the prepared state)
"expectation_values": [[...]], # shape (n_times, n_observables)
"observable_labels": [...], # for example: ["Z_0", "ZZ_0_1"]
"metadata": {
"n", "t_steps", "dt", "tier",
"aqc_compressed_steps": 5, # total compressed steps (= sum of segment n_steps)
"aqc_segments": [ # per segment: the plan plus its own results
{"n_steps": 3, "ansatz_steps": 1, "steps": [1, 2, 3], "n_params": 133,
"fidelities": {"1": ..., "2": ..., "3": ...}},
{"n_steps": 2, "ansatz_steps": 2, "steps": [4, 5], "n_params": 245,
"fidelities": {"4": ..., "5": ...}},
],
"execution_backend",
"aqc_fidelities": {"1": ..., "2": ...}, # flat per-step fidelity, all compressed steps
"circuit_stats": { # per-step 2q depth and gate count, full Trotter vs AQC
"1": {"full_trotter": {"depth_2q": ..., "num_2q_gates": ...},
"aqc_trotter": {"depth_2q": ..., "num_2q_gates": ...}},
"2": {...},
},
"warnings": [...], # non-fatal notices; for example, a cotengrust fallback
"resource_usage": { # per stage; QPU_TIME is the charged QPU time
"RUNNING: OPTIMIZING_FOR_HARDWARE": {"CPU_TIME": ...},
"RUNNING: WAITING_FOR_QPU": {"CPU_TIME": ...},
"RUNNING: EXECUTING_QPU": {"QPU_TIME": ...},
},
},
# present only when return_circuits=True
"circuits": [QuantumCircuit, ...], # one per evolved step; circuits[i] is at times[i + 1]
}

aqc_fidelities y circuit_stats son los dos que hay que leer primero: juntos indican si la compresión se mantuvo fiel y si realmente ahorró profundidad. En runtime, resource_usage reporta la espera en cola por separado del tiempo de QPU que se te cobra. Una entrada rechazada falla rápido como un ServerlessError estructurado (código 4615).

Ejemplo de simulador

Ejecuta la función primero en el backend exacto statevector. No consume tiempo de QPU y valida el despliegue de extremo a extremo. El modelo aquí es una cadena de Ising de campo transversal de ocho qubits, y se omite observables para que la función mida el ZZ predeterminado por sitio.

El plan de compresión es la entrada que vale la pena entender. Cada segmento {"n_steps": k, "ansatz_steps": m} comprime k pasos de Trotter consecutivos en un ansatz construido a partir de un objetivo de Trotter de m pasos, y cualquier paso más allá de sum(n_steps) se ejecuta como Trotter simple. Los pasos tempranos, de baja entrelazamiento, se comprimen bien en un ansatz de una sola capa poco profunda; los pasos posteriores, más entrelazados, necesitan uno más profundo.

from qiskit.quantum_info import SparsePauliOp

fn = serverless.load("aqc-dynamics-function")

n = 8
H = SparsePauliOp.from_sparse_list(
[("ZZ", [i, i + 1], 1.0) for i in range(n - 1)]
+ [("X", [i], 0.8) for i in range(n)],
num_qubits=n,
)

job = fn.run(
t_steps=8,
aqc_segments=[
{
"n_steps": 4,
"ansatz_steps": 1,
}, # early steps -> shallow 1-layer ansatz
{
"n_steps": 2,
"ansatz_steps": 2,
}, # later steps -> deeper 2-layer ansatz
],
hamiltonian=H,
aqc_options={"max_bond": 32},
backend="statevector",
)
print("job ID:", job.job_id)
job ID: ee1f3793-e995-427d-81d1-5924549beb38

Sigue la ejecución y lee el resultado

status() reporta tanto el ciclo de vida general del trabajo como el subestado por etapa que la función publica a medida que se ejecuta. Las mismas etapas se aplican a la ejecución en hardware más adelante en esta guía:

QUEUED -> INITIALIZING -> RUNNING: OPTIMIZING_FOR_HARDWARE -> RUNNING: WAITING_FOR_QPU -> RUNNING: EXECUTING_QPU -> RUNNING: POST_PROCESSING -> DONE

valor de status()Etapa
RUNNING: OPTIMIZING_FOR_HARDWAREpreparación del estado, construcción de Trotter, compresión AQC
RUNNING: WAITING_FOR_QPUen cola en el QPU (solo backend runtime)
RUNNING: EXECUTING_QPUcircuitos en ejecución (los simuladores locales marcan esto directamente)
RUNNING: POST_PROCESSINGensamblando el diccionario de resultados

Los estados terminales son DONE, ERROR, y CANCELED. Esta ejecución de statevector no tiene cola de QPU, por lo que omite RUNNING: WAITING_FOR_QPU. Usa job.logs() en cualquier momento para ver los registros por etapa, incluida la fidelidad AQC alcanzada en cada paso.

print(job.status()) # re-run until this reports DONE
DONE
import numpy as np

result = job.result()
ev = np.array(result["expectation_values"])

print("observables:", result["observable_labels"])
print("shape:", ev.shape, "-> (n_times, n_observables)")
print("first row (t = 0, the prepared state):", np.round(ev[0], 4))
print("last row (t = t_steps * dt):", np.round(ev[-1], 4))
print(
"AQC fidelities:",
{k: round(v, 4) for k, v in result["metadata"]["aqc_fidelities"].items()},
)

# What the compression bought: 2-qubit depth at the final time step.
stats = result["metadata"]["circuit_stats"][
str(result["metadata"]["t_steps"])
]
print(
"2q depth at the final step:",
stats["full_trotter"]["depth_2q"],
"(full Trotter) ->",
stats["aqc_trotter"]["depth_2q"],
"(AQC + Trotter)",
)
observables: ['Z_0', 'Z_1', 'Z_2', 'Z_3', 'Z_4', 'Z_5', 'Z_6', 'Z_7']
shape: (9, 8) -> (n_times, n_observables)
first row (t = 0, the prepared state): [1. 1. 1. 1. 1. 1. 1. 1.]
last row (t = t_steps * dt): [0.1442 0.2956 0.4686 0.4877 0.4869 0.4686 0.2963 0.1441]
AQC fidelities: {'1': 1.0, '2': 1.0, '3': 1.0, '4': 1.0, '5': 1.0, '6': 0.9999}
2q depth at the final step: 210 (full Trotter) -> 79 (AQC + Trotter)

Ejemplo de hardware

Una llamada de función con backend="runtime" transpila y se ejecuta en un procesador real de IBM Quantum, con la mitigación de errores integrada de la función: desacoplamiento dinámico (XY4), twirling de puertas, y extinción de error de lectura por twirling (TREX). backend_name selecciona el dispositivo; si se omite, la función toma el menos ocupado.

Nada cambia respecto al código científico. Lo que difiere del ejemplo del simulador es la longitud de la cadena, el número de pasos de Trotter, el plan de compresión, el backend, y la configuración de mitigación explícita cubierta en la siguiente sección.

Dimensionar el trabajo para el hardware de control

estimator_options es la entrada que vale la pena configurar deliberadamente. El twirling de puertas construye num_randomizations circuitos aleatorizados separados para cada PUB, y todo el trabajo, cada PUB con todas sus aleatorizaciones, tiene que caber en la memoria de instrucciones del sistema de control clásico del QPU. La función usa 1000 aleatorizaciones por defecto, por lo que una evolución de 10 pasos envía 11 PUB de 1000 circuitos cada uno: aproximadamente 11.000 instancias de circuito en un solo trabajo.

Si se excede lo que el sistema de control puede contener, el trabajo falla con el error 6073. Límites de trabajo proporciona los umbrales y cómo contar contra ellos, siendo el principal 26,8 millones de instrucciones del sistema de control por qubit, aplicado por trabajo en lugar de por PUB. El desacoplamiento dinámico añade puertas que cuentan para ello.

Dos entradas controlan el tamaño:

  • estimator_options establece el presupuesto de shots. Los shots totales son num_randomizations * shots_per_randomization, por lo que puedes intercambiar aleatorizaciones por shots por aleatorización, mantener las estadísticas, y aun así reducir el programa. La siguiente celda usa 100 aleatorizaciones a 200 shots cada una, que son 20.000 shots por observable y aproximadamente una décima parte de las instancias de circuito que enviarían los valores predeterminados. Consulta TwirlingOptions y Opciones de Estimator para el conjunto completo de campos.

  • batches divide los PUB entre esa cantidad de trabajos de runtime separados, que es el remedio que el propio error 6073 sugiere y por qué importa el enfoque por trabajo. Establecer batches=4 envía aproximadamente tres PUB por trabajo en lugar de once a la vez, y los trabajos salen juntos en un lote, por lo que el grupo entra en cola una sola vez en lugar de que cada trabajo entre en cola por separado.

Recuerda que un estimator_options proporcionado reemplaza los valores predeterminados de la función por completo en lugar de fusionarse con ellos, así que el desacoplamiento dinámico y TREX se reafirman en la siguiente celda para mantenerlos activados.

from qiskit.quantum_info import SparsePauliOp

fn = serverless.load("aqc-dynamics-function")

n = 10
H = SparsePauliOp.from_sparse_list(
[("ZZ", [i, i + 1], 1.0) for i in range(n - 1)]
+ [("X", [i], 0.8) for i in range(n)],
num_qubits=n,
)

job = fn.run(
t_steps=10,
aqc_segments=[
{
"n_steps": 3,
"ansatz_steps": 1,
}, # early steps -> shallow 1-layer ansatz
{
"n_steps": 3,
"ansatz_steps": 2,
}, # later steps -> deeper 2-layer ansatz
],
hamiltonian=H,
aqc_options={"max_bond": 32},
backend="runtime",
backend_name="ibm_marrakesh",
# The function defaults to 1000 twirling randomizations, which was too large
# for this device. Total shots is num_randomizations *
# shots_per_randomization, so this is 20,000 shots per observable.
estimator_options={
"dynamical_decoupling": {"enable": True, "sequence_type": "XY4"},
"twirling": {
"enable_gates": True,
"num_randomizations": 100,
"shots_per_randomization": 200,
},
"resilience": {"measure_mitigation": True},
},
)
print("job ID (save this to reconnect later):", job.job_id)
job ID (save this to reconnect later): 7229a8bf-9f83-4785-8dd4-489844abc2d9
Reconectar a un trabajo de larga duración

Una ejecución en hardware no es rápida, y la mayor parte del tiempo es clásico en lugar de en el QPU. La compresión AQC se ejecuta dentro de la función antes de que nada llegue al QPU, y la cola del QPU se suma a eso. No necesitas mantener abierto este cuaderno o kernel mientras se ejecuta.

Copia el ID de trabajo impreso por la celda anterior y guárdalo. Las siguientes tres celdas te permiten retomar la ejecución más tarde:

  1. Reconectar, solo necesario en una nueva sesión de kernel: vuelve a ejecutar la celda de Autenticación para recrear serverless, luego reconstruye el manejador job a partir del ID que guardaste. Omite esta celda si sigues en la sesión donde enviaste el trabajo, porque el manejador ya está activo.

  2. Verificar el estado: vuelve a ejecutar hasta que reporte DONE.

  3. Obtener el resultado: ejecuta solo una vez que el estado sea DONE.

Pega tu ID guardado sobre el marcador de posición en la siguiente celda de reconexión.

# Reconnect to a previously submitted job by its ID. Only needed in a NEW kernel
# session; if you are still in the session where you submitted, the `job` handle
# from the preceding cell is already live, so skip this cell. Replace the ID that follows with your own.
job = serverless.get_job_by_id("<your job ID>")
# Re-run this until it reports DONE, then fetch the result in the following cell.
print(job.status())
DONE
import numpy as np

# Run this only once the preceding status cell reports DONE. result() blocks until
# the job finishes, so calling it earlier just waits.
result = job.result()
ev = np.array(result["expectation_values"])

print("backend:", result["metadata"]["execution_backend"])
print("shape:", ev.shape, "-> (n_times, n_observables)")
print("last row (t = t_steps * dt):", np.round(ev[-1], 4))
print(
"AQC fidelities:",
{k: round(v, 4) for k, v in result["metadata"]["aqc_fidelities"].items()},
)

# What the compression bought: 2-qubit depth at the final time step.
stats = result["metadata"]["circuit_stats"][
str(result["metadata"]["t_steps"])
]
print(
"2q depth at the final step:",
stats["full_trotter"]["depth_2q"],
"(full Trotter) ->",
stats["aqc_trotter"]["depth_2q"],
"(AQC + Trotter)",
)
backend: runtime
shape: (11, 10) -> (n_times, n_observables)
last row (t = t_steps * dt): [0.1504 0.1361 0.218 0.2144 0.2275 0.1783 0.1749 0.1599 0.0915 0.0922]
AQC fidelities: {'1': 1.0, '2': 1.0, '3': 1.0, '4': 1.0, '5': 0.9999, '6': 0.9999}
2q depth at the final step: 342 (full Trotter) -> 171 (AQC + Trotter)

Próximos pasos

Recomendaciones