Hola AHS: ejecución de la primera simulación hamiltoniana analógica - Amazon Braket

Las traducciones son generadas a través de traducción automática. En caso de conflicto entre la traducción y la version original de inglés, prevalecerá la version en inglés.

Hola AHS: ejecución de la primera simulación hamiltoniana analógica

En esta sección se proporciona información sobre cómo ejecutar su primera simulación hamiltoniana analógica.

Cadena de espín interactiva

Como ejemplo canónico de un sistema de muchas partículas que interactúan, consideremos un anillo de ocho espines (cada uno de los cuales puede estar en estados «arriba» y «abajo»). Aunque pequeño, este sistema modelo ya muestra una serie de fenómenos interesantes propios de los materiales magnéticos naturales. En este ejemplo, mostraremos cómo preparar un orden denominado antiferromagnético, en el que los espines consecutivos apuntan en direcciones opuestas.

Diagrama que conecta 8 nodos circulares que contienen flechas inversas hacia arriba y hacia abajo.

Disposición

Utilizaremos un átomo neutro para representar cada espín, y los estados de espín «arriba» y «abajo» se codificarán en el estado excitado de Rydberg y el estado fundamental de los átomos, respectivamente. En primer lugar, crearemos la disposición 2-D. Podemos programar el anillo de espines anterior con el siguiente código.

Requisitos previos: es necesario instalar el SDK de Braket. (Si utiliza una instancia de cuaderno alojada en Braket, este SDK viene preinstalado con los cuadernos). Para reproducir los gráficos, también debe instalar matplotlib por separado con el intérprete de comandos pip install matplotlib.

from braket.ahs.atom_arrangement import AtomArrangement import numpy as np import matplotlib.pyplot as plt # Required for plotting a = 5.7e-6 # Nearest-neighbor separation (in meters) register = AtomArrangement() register.add(np.array([0.5, 0.5 + 1/np.sqrt(2)]) * a) register.add(np.array([0.5 + 1/np.sqrt(2), 0.5]) * a) register.add(np.array([0.5 + 1/np.sqrt(2), - 0.5]) * a) register.add(np.array([0.5, - 0.5 - 1/np.sqrt(2)]) * a) register.add(np.array([-0.5, - 0.5 - 1/np.sqrt(2)]) * a) register.add(np.array([-0.5 - 1/np.sqrt(2), - 0.5]) * a) register.add(np.array([-0.5 - 1/np.sqrt(2), 0.5]) * a) register.add(np.array([-0.5, 0.5 + 1/np.sqrt(2)]) * a)

que también podemos representar gráficamente con:

fig, ax = plt.subplots(1, 1, figsize=(7, 7)) xs, ys = [register.coordinate_list(dim) for dim in (0, 1)] ax.plot(xs, ys, 'r.', ms=15) for idx, (x, y) in enumerate(zip(xs, ys)): ax.text(x, y, f" {idx}", fontsize=12) plt.show() # This will show the plot below in an ipython or jupyter session
Gráfico de dispersión que muestra puntos distribuidos entre valores positivos y negativos en ambos ejes.

Interacción

Para preparar la fase antiferromagnética, necesitamos inducir interacciones entre espines vecinos. Para ello utilizamos la interacción de van der Waals, que se implementa de forma nativa mediante dispositivos de átomos neutros (como el dispositivo Aquila de QuEra). Utilizando la representación de espines, el término hamiltoniano para esta interacción puede expresarse como una suma de todos los pares de espines (j, k).

Ecuación de interacción hamiltoniana que muestra esta interacción expresada como una suma de todos los pares de espines (j, k).

Aquí, nj​=∣↑j​⟩⟨↑j​∣es un operador que toma el valor 1 solo si el espín j está en el estado «arriba», y 0 en caso contrario. La fuerza es Vj,k​=C6​/(dj,k​)6, donde C6​ es el coeficiente fijo y dj,k​ es la distancia euclidiana entre los espines j y k. El efecto inmediato de este término de interacción es que cualquier estado en el que tanto el espín j como el espín k estén «arriba» tienen una energía elevada (con la cantidad Vj,k​). Al diseñar cuidadosamente el resto del programa de AHS, esta interacción evitará que los espines vecinos se encuentren ambos en el estado «arriba», un efecto conocido comúnmente como «bloqueo de Rydberg».

Campo de accionamiento

Al inicio del programa de AHS, todos los espines (por defecto) comienzan en su estado «abajo», es decir, se encuentran en la denominada fase ferromagnética. Con la mirada puesta en nuestro objetivo de preparar la fase antiferromagnética, especificamos un campo de excitación coherente dependiente del tiempo que hace que los espines pasen suavemente de este estado a un estado de muchos cuerpos en el que se prefieren los estados «arriba». El hamiltoniano correspondiente se puede escribir como:

Ecuación matemática que representa el cálculo de una función de accionamiento hamiltoniano.

donde Ω(t),ϕ(t),Δ(t) son la amplitud global dependiente del tiempo (también conocida como frecuencia de Rabi), la fase y la desintonización del campo de accionamiento que afecta a todos los espines de manera uniforme. Aquí, S−,k​=∣↓k​⟩⟨↑k​∣and S+,k​​=(S−,k​)=∣↑k​⟩⟨↓k​∣son los operadores de bajada y subida del espín k, respectivamente, y nk​=∣↑k​⟩⟨↑k​∣es el mismo operador que antes. La parte Ω del campo de accionamiento acopla de forma coherente los estados «abajo» y «arriba» de todos los espines simultáneamente, mientras que la parte Δ controla la recompensa energética para los estados «arriba».

Para programar una transición suave de la fase ferromagnética a la fase antiferromagnética, especificamos el campo de excitación con el siguiente código.

from braket.timings.time_series import TimeSeries from braket.ahs.driving_field import DrivingField # Smooth transition from "down" to "up" state time_max = 4e-6 # seconds time_ramp = 1e-7 # seconds omega_max = 6300000.0 # rad / sec delta_start = -5 * omega_max delta_end = 5 * omega_max omega = TimeSeries() omega.put(0.0, 0.0) omega.put(time_ramp, omega_max) omega.put(time_max - time_ramp, omega_max) omega.put(time_max, 0.0) delta = TimeSeries() delta.put(0.0, delta_start) delta.put(time_ramp, delta_start) delta.put(time_max - time_ramp, delta_end) delta.put(time_max, delta_end) phi = TimeSeries().put(0.0, 0.0).put(time_max, 0.0) drive = DrivingField( amplitude=omega, phase=phi, detuning=delta )

Podemos visualizar la serie temporal del campo de accionamiento con el siguiente script.

fig, axes = plt.subplots(3, 1, figsize=(12, 7), sharex=True) ax = axes[0] time_series = drive.amplitude.time_series ax.plot(time_series.times(), time_series.values(), '.-') ax.grid() ax.set_ylabel('Omega [rad/s]') ax = axes[1] time_series = drive.detuning.time_series ax.plot(time_series.times(), time_series.values(), '.-') ax.grid() ax.set_ylabel('Delta [rad/s]') ax = axes[2] time_series = drive.phase.time_series # Note: time series of phase is understood as a piecewise constant function ax.step(time_series.times(), time_series.values(), '.-', where='post') ax.set_ylabel('phi [rad]') ax.grid() ax.set_xlabel('time [s]') plt.show() # This will show the plot below in an ipython or jupyter session
Tres gráficos que muestran phi, delta y omega a lo largo del tiempo. La subgráfica superior muestra el crecimiento justo por encima de 6, rads/s donde permanece durante 4 segundos hasta que vuelve a caer a 0. El subgráfico del medio muestra el crecimiento lineal asociado de la derivada, y el subgráfico inferior ilustra una línea plana cercana a cero.

Programa de AHS

El registro, el campo de accionamiento (y las interacciones implícitas de van der Waals) conforman el programa de simulación hamiltoniana analógica ahs_program.

from braket.ahs.analog_hamiltonian_simulation import AnalogHamiltonianSimulation ahs_program = AnalogHamiltonianSimulation( register=register, hamiltonian=drive )

Ejecución en un simulador local

Dado que este ejemplo es pequeño (menos de 15 espines), antes de ejecutarlo en una QPU compatible con AHS, podemos ejecutarlo en el simulador AHS local que viene con el SDK de Braket. Puesto que el simulador local está disponible de forma gratuita con el SDK de Braket, esta es la mejor práctica para garantizar que nuestro código se ejecute correctamente.

Aquí, podemos establecer el número de shots en un valor alto (por ejemplo, 1 millón) porque el simulador local realiza un rastreo de la evolución temporal del estado cuántico y extrae muestras del estado final; por lo tanto, al aumentar el número de shots, el tiempo total de ejecución solo aumenta ligeramente.

from braket.devices import LocalSimulator device = LocalSimulator("braket_ahs") result_simulator = device.run( ahs_program, shots=1_000_000 ).result() # Takes about 5 seconds

Análisis de resultados de simuladores

Podemos agregar los resultados de los lanzamientos con la siguiente función que infiere el estado de cada espín (que puede ser «d» para «abajo», «u» para «arriba» o «e» para sitio vacío) y cuenta cuántas veces se produjo cada configuración en los shots.

from collections import Counter def get_counts(result): """Aggregate state counts from AHS shot results A count of strings (of length = # of spins) are returned, where each character denotes the state of a spin (site): e: empty site u: up state spin d: down state spin Args: result (braket.tasks.analog_hamiltonian_simulation_quantum_task_result.AnalogHamiltonianSimulationQuantumTaskResult) Returns dict: number of times each state configuration is measured """ state_counts = Counter() states = ['e', 'u', 'd'] for shot in result.measurements: pre = shot.pre_sequence post = shot.post_sequence state_idx = np.array(pre) * (1 + np.array(post)) state = "".join(map(lambda s_idx: states[s_idx], state_idx)) state_counts.update((state,)) return dict(state_counts) counts_simulator = get_counts(result_simulator) # Takes about 5 seconds print(counts_simulator)
*[Output]* {'dddddddd': 5, 'dddddddu': 12, 'ddddddud': 15, ...}

Este counts es un diccionario que cuenta el número de veces que se observa cada configuración de estado en los shots. También podemos visualizarlos con el siguiente código.

from collections import Counter def has_neighboring_up_states(state): if 'uu' in state: return True if state[0] == 'u' and state[-1] == 'u': return True return False def number_of_up_states(state): return Counter(state)['u'] def plot_counts(counts): non_blockaded = [] blockaded = [] for state, count in counts.items(): if not has_neighboring_up_states(state): collection = non_blockaded else: collection = blockaded collection.append((state, count, number_of_up_states(state))) blockaded.sort(key=lambda _: _[1], reverse=True) non_blockaded.sort(key=lambda _: _[1], reverse=True) for configurations, name in zip((non_blockaded, blockaded), ('no neighboring "up" states', 'some neighboring "up" states')): plt.figure(figsize=(14, 3)) plt.bar(range(len(configurations)), [item[1] for item in configurations]) plt.xticks(range(len(configurations))) plt.gca().set_xticklabels([item[0] for item in configurations], rotation=90) plt.ylabel('shots') plt.grid(axis='y') plt.title(f'{name} configurations') plt.show() plot_counts(counts_simulator)
Gráfico de barras que muestra un gran número de shots sin configuraciones de estados «arriba» adyacentes.
Gráfico de barras que muestra los shots de algunas configuraciones de estados «arriba» vecinos, con 4 estados a 1,0 shots.

A partir de los gráficos, podemos extraer las siguientes observaciones que confirman que hemos preparado correctamente la fase antiferromagnética.

  1. Por lo general, los estados no bloqueados (en los que no hay dos espines vecinos en estado «arriba») son más comunes que los estados en los que al menos un par de espines vecinos se encuentran ambos en estado «arriba».

  2. Normalmente, se prefieren los estados con más excitaciones «arriba», a menos que la configuración esté bloqueada.

  3. Los estados más comunes son, de hecho, los estados antiferromagnéticos perfectos "dudududu" y "udududud".

  4. Los segundos estados más comunes son aquellos en los que solo hay tres excitaciones «arriba» con separaciones consecutivas de 1, 2, 2. Esto demuestra que la interacción de van der Waals también afecta (aunque en menor medida) a los vecinos más cercanos.

Se ejecuta en la QuEra QPU Aquila

Requisitos previos: además de instalar el SDK de Braket con pip, si es nuevo en Amazon Braket, asegúrese de haber completado los pasos de introducción.

nota

Si utiliza una instancia de cuaderno alojada en Braket, el SDK de Braket viene preinstalado con la instancia.

Con todas las dependencias instaladas, podemos conectarnos a la QPU de Aquila.

from braket.aws import AwsDevice aquila_qpu = AwsDevice("arn:aws:braket:us-east-1::device/qpu/quera/Aquila")

Para que nuestro programa de AHS sea adecuado para la máquina de QuEra, necesitamos redondear todos los valores para que cumplan con los niveles de precisión permitidos por la QPU de Aquila. (Estos requisitos se rigen por los parámetros del dispositivo con «Resolución» en su nombre. Podemos verlos al ejecutar aquila_qpu.properties.dict() en un cuaderno. Para obtener más detalles sobre las capacidades y requisitos de Aquila, consulte el cuaderno Introducción a Aquila). Podemos hacerlo llamando al método discretize.

discretized_ahs_program = ahs_program.discretize(aquila_qpu)

Ahora podemos ejecutar el programa (ejecutando solo 100 shots de momento) en la QPU de Aquila.

nota

La ejecución de este programa en el procesador Aquila tendrá un costo. El SDK de Amazon Braket incluye un Rastreador de costos que permite a los clientes establecer límites de costos y realizar el seguimiento de sus costos casi en tiempo real.

task = aquila_qpu.run(discretized_ahs_program, shots=100) metadata = task.metadata() task_arn = metadata['quantumTaskArn'] task_status = metadata['status'] print(f"ARN: {task_arn}") print(f"status: {task_status}")
*[Output]* ARN: arn:aws:braket:us-east-1:123456789012:quantum-task/12345678-90ab-cdef-1234-567890abcdef status: CREATED

Debido a la gran variación en el tiempo que puede tardar en ejecutarse una tarea cuántica (dependiendo de los periodos de disponibilidad y la utilización de la QPU), es recomendable anotar el ARN de la tarea cuántica, para poder comprobar su estado más adelante con el siguiente fragmento de código.

# Optionally, in a new python session from braket.aws import AwsQuantumTask SAVED_TASK_ARN = "arn:aws:braket:us-east-1:123456789012:quantum-task/12345678-90ab-cdef-1234-567890abcdef" task = AwsQuantumTask(arn=SAVED_TASK_ARN) metadata = task.metadata() task_arn = metadata['quantumTaskArn'] task_status = metadata['status'] print(f"ARN: {task_arn}") print(f"status: {task_status}")
*[Output]* ARN: arn:aws:braket:us-east-1:123456789012:quantum-task/12345678-90ab-cdef-1234-567890abcdef status: COMPLETED

Una vez que el estado sea COMPLETADO (lo cual también se puede comprobar desde la página de tareas cuánticas de la consola de Amazon Braket), podemos consultar los resultados con:

result_aquila = task.result()

Análisis de los resultados de la QPU

Usando las mismas funciones get_counts que antes, podemos computar los recuentos:

counts_aquila = get_counts(result_aquila) print(counts_aquila)
*[Output]* {'dddududd': 2, 'dudududu': 18, 'ddududud': 4, ...}

y representarlos gráficamente con plot_counts:

plot_counts(counts_aquila)
Gráfico de barras que muestra un gran número de shots sin configuraciones de estados «arriba» adyacentes.
Gráfico de barras que muestra los shots de algunas configuraciones de estados «arriba» vecinos, con 4 estados a 1,0 shots.

Tenga en cuenta que una pequeña fracción de los shots tiene sitios vacíos (marcados con una «e»). Esto se debe a que la QPU de Aquila presenta imperfecciones en la preparación de un 1-2 % por átomo. Además, los resultados coinciden con los de la simulación dentro de la fluctuación estadística esperada debido al reducido número de shots.

Siguientes pasos

Enhorabuena, ya ha ejecutado su primera carga de trabajo de AHS en Amazon Braket con el simulador AHS local y la QPU de Aquila.

Para obtener más información sobre la física de Rydberg, la simulación hamiltoniana analógica y el dispositivo Aquila, consulte nuestros cuadernos de ejemplos.