SqDRIFT algoritmo para la estimación del estado fundamental
Descarga el cuaderno « Python » para seguir este tutorial por tu cuenta. El portátil descarga el archivo de entrada de FCIDump si aún no está presente.
Estimación de tiempo de ejecución: 180 segundos en un procesador Heron r3 (NOTA: Se trata únicamente de una estimación. (El tiempo de ejecución puede variar.)
Resultados del aprendizaje
- Descubre cómo crear circuitos con menor profundidad en comparación con la técnica de Trotter
- Recorre un flujo de trabajo completo para la estimación del estado fundamental utilizando « qDRIFT » y SQD
- Descubre cómo utilizarlo
qiskit-fermionsjunto con otros complementos de Qiskit para implementar un flujo de trabajo de este tipo
Requisitos previos
- Lee la descripción general de la diagonalización cuántica basada en muestras (SQD)
- Lee la lección sobre la diagonalización cuántica de Krylov basada en muestras (SKQD)
En segundo plano
SqDRIFT es una variante del SKQD que sustituye la necesidad de elegir un ansatz a partir del cual muestrear cadenas de bits por un conjunto de circuitos de evolución temporal construidos directamente a partir del hamiltoniano objetivo. Esto se consigue mediante el submuestreo de operadores de evolución temporal más pequeños a partir del hamiltoniano, basándose en sus coeficientes, lo que se conoce como el método de « qDRIFT » de Trotterización.
Ambas implementaciones utilizan Qiskit Fermions para representar el hamiltoniano molecular y construir circuitos para el algoritmo « qDRIFT ». La implementación de « Python » utiliza pasadas del transpilador fermiónico, mientras que la implementación en C++ muestrea explícitamente los grupos y los asigna a operadores de qubits.
Supongamos que el hamiltoniano tiene la forma:
donde, sin pérdida de generalidad, se exige que y que el mayor valor propio de sea igual, en valor absoluto, a . Cualquier prefactor con signo o complejo se absorbe en , por lo que los coeficientes son pesos estrictamente positivos, mientras que los determinan la dirección de cada término. Aquí, es el número de términos (o, tras la agrupación, el número de grupos) del hamiltoniano; se trata de una propiedad del hamiltoniano y es distinto del número de operadores muestreados en un único circuito, que a continuación se denota c .
A continuación, el algoritmo « qDRIFT » realiza, para el tiempo objetivo , un operador , donde va desde y representa el circuito SqDRIFT, definido como:
Aquí, es el número de operadores muestreados por circuito y es el número de circuitos del conjunto. El producto se calcula sobre los sorteos de , no sobre todos los términos hamiltonianos de , y, dado que los términos se extraen con repetición, el mismo puede aparecer más de una vez en un único .
La cantidad:
es la norma « » de los coeficientes, por lo que cada uno de los pasos « » evoluciona durante el mismo tiempo « », independientemente del término que se haya extraído. La uniformidad del ángulo de paso es la característica distintiva de qDRIFT: : un coeficiente influye en el resultado en función de la frecuencia con la que se extrae su término, y no en función del ángulo de rotación de dicho término. Los índices se extraen de la distribución:
Por lo tanto, la serie « » es una secuencia aleatoria de índices de términos extraídos de esta distribución. Dado que los valores de « » son positivos y su suma es « », se trata de una distribución de probabilidad normalizada, y la esperanza del canal resultante a partir de los muestreos aleatorios se aproxima a la evolución bajo « », con un error que disminuye a medida que aumenta « ». Ten en cuenta que el error de aproximación depende de y no del número de términos .
(En el artículo « SqDRIFT » se denota el número de términos como « » y la longitud de la sucesión como « »; aquí utilizamos « » y « » para diferenciar claramente ambos conceptos.)
Este tutorial muestra cómo generar un conjunto de circuitos aleatorios de este tipo. Una vez creados estos circuitos, de forma similar a como se crea un subespacio de Krylov para diferentes operadores, tomamos muestras de cadenas de bits de varios de esos operadores con distintos parámetros temporales. Esto garantiza una mayor superposición entre los vectores del estado fundamental y las cadenas de bits muestreadas.
Requisitos
Antes de empezar este tutorial, asegúrate de que tienes instalado
- Un entorno virtual de Python (>= 3.10 )
- pip>=25.1
qiskit~= 2.5qiskit-fermions== 0.1.0 (Ten en cuenta que el nombre está en plural)- numpy
- pyscf
qiskit-aerqiskit-ibm-runtimeqiskit-addon-sqd
Puedes instalar todos los paquetes necesarios con:
pip install "qiskit~=2.5" "qiskit-fermions==0.1.0" qiskit-aer qiskit-ibm-runtime qiskit-addon-sqd pyscf numpy
Configuración
# Third-party scientific computing
import numpy as np
# PySCF
from pyscf import tools, ao2mo, fci
# Qiskit core
from qiskit import transpile
from qiskit.primitives import BitArray
# Qiskit Aer
from qiskit_aer import AerSimulator
# IBM Quantum Compute Service
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
# Qiskit Fermions
from qiskit_fermions.operators.library import FCIDump
from qiskit_fermions.operators import FermionOperator
from qiskit_fermions.operators.terms.filtering import filter_diagonal_terms
from qiskit_fermions.operators.terms.grouping import (
group_terms_by_electronic_structure,
)
from qiskit_fermions.operators.terms.ordering import canonical_order
from qiskit_fermions.circuit import FermionicCircuit
from qiskit_fermions.circuit.library import Evolution
from qiskit_fermions.transpiler import FermionicPassManager
from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
from qiskit_fermions.transpiler.passes import QDriftTrotterization
from qiskit_fermions.circuit.library import InitializeModes
# Qiskit addon SQD
from qiskit_addon_sqd.fermion import (
diagonalize_fermionic_hamiltonian,
SCIResult,
)Implementación
Paso 1: Asignar entradas clásicas a un problema cuántico
Los pasos que se indican a continuación en Python utilizan el simulador Aer. A continuación se muestra el ejemplo de hardware.
Cómo leer y preparar el FCIDump
Para este tutorial, cargaremos el hamiltoniano de la estructura electrónica del nitrógeno ( N2 ). También hay otras formas de crear operadores fermiónicos. Consulta la documentación en qiskit_fermions.operators.library.
Acerca de este FCIDump. El archivo describe N2_sto_3g una molécula de nitrógeno ( ) en la base mínima « STO-3G » con una separación interatómica de 1.09 , que es la longitud de enlace de equilibrio experimental. En su encabezado se declaran NORB=10, NELEC=14, y MS2=0: 10 orbitales espaciales (por lo tanto, 20 orbitales de espín y 20 qubits según la representación de Jordan-Wigner), 14 electrones en un singulete de espín, es decir, siete electrones y siete . A todos los orbitales se les asigna la etiqueta de simetría 1, es decir, no se aprovecha ninguna simetría de grupo puntual. Al tratarse de un volcado de « STO-3G » en todo el espacio, no hay orbitales congelados y el espacio de correlación es lo suficientemente pequeño como para poder calcular de forma clásica una energía de referencia FCI exacta a efectos comparativos, tal y como se muestra en el siguiente bloque de código.
Se puede volver a generar un archivo equivalente con el comando « PySCF: »
from pyscf import gto, scf, tools
mol = gto.M(atom="N 0 0 0; N 0 0 1.09", basis="sto-3g", symmetry=False)
mf = scf.RHF(mol).run()
tools.fcidump.from_scf(mf, "N2_sto_3g")Dado que las integrales dependen de los orbitales SCF convergentes, un archivo regenerado podría diferir del original en cuanto a la fase o el orden de los orbitales; las energías totales no se ven afectadas.
Cómo obtener el archivo. Encuentra el FCIDump en este repositorio de GitHub. Puedes ejecutar el código que aparece a continuación para descargarlo en la ubicación que indica el resto del tutorial.
En primer lugar, utilizamos la función proporcionada cisolver por pyscf para obtener la energía de referencia. Esta es la energía real del estado fundamental de la molécula con la que estamos trabajando. Para ello, primero definiremos norb y nelec, que son el número de orbitales y el número de electrones, respectivamente. A continuación, definimos y h1e h2e, que son las integrales de un y dos electrones, respectivamente. Todo esto se utilizará posteriormente también para el SQD.
import os
from urllib.request import urlopen
# The FCIDump is stored with this tutorial in the Qiskit documentation repository.
FCIDUMP_URL = "https://raw.githubusercontent.com/Qiskit/documentation/main/docs/tutorials/assets/sqdrift/fcidump_files/N2_sto_3g"
FCIDUMP_PATH = "fcidump_files/N2_sto_3g"
if not os.path.exists(FCIDUMP_PATH):
os.makedirs(os.path.dirname(FCIDUMP_PATH), exist_ok=True)
with urlopen(FCIDUMP_URL) as response:
contents = response.read()
with open(FCIDUMP_PATH, "wb") as f:
f.write(contents)
print(f"Downloaded FCIDump to {FCIDUMP_PATH}")
else:
print(f"Using existing FCIDump at {FCIDUMP_PATH}")name = "fcidump_files/N2_sto_3g"
fcidump = tools.fcidump.read(name)
# Extract metadata from the FCIDump header
norb = fcidump["NORB"] # number of spatial orbitals
nelec = fcidump["NELEC"] # total number of electrons
e_nuc = fcidump["ECORE"] # nuclear repulsion / core energy
ms2 = fcidump["MS2"] # 2S (spin)
num_elec_a = (nelec + ms2) // 2 # alpha electrons
num_elec_b = (nelec - ms2) // 2 # beta electrons
# Reconstruct full 4-index ERIs from the FCIDump (stored in 8-fold symmetry)
h1e = fcidump["H1"] # shape (norb, norb)
h2e = ao2mo.restore( # shape (norb, norb, norb, norb)
1, fcidump["H2"], norb
)
cisolver = fci.direct_spin1.FCI()
cisolver.max_cycle = 200
cisolver.conv_tol = 1e-12
e_fci, _ = cisolver.kernel(
h1e,
h2e,
norb,
(num_elec_a, num_elec_b),
ecore=e_nuc, # adds nuclear repulsion to the final energy
)
reference_energy = e_fci
print(f"Reference FCI Energy = {reference_energy:.10f} Ha")
nuclear_repulsion_energy = fcidump["ECORE"]
print(f"Nuclear Repulsion Energy = {nuclear_repulsion_energy:.10f} Ha")Reference FCI Energy = -107.6481842917 Ha
Nuclear Repulsion Energy = 23.7887003074 HaCarga del hamiltoniano
Una vez que disponemos de los datos necesarios, leemos el hamiltoniano del archivo FCI en un formato compatible con qiskit-fermions
fcidump = FCIDump.from_file(name)
hamiltonian = FermionOperator.from_fcidump(fcidump)
num_modes = 2 * fcidump.norbFlujos de trabajo sobre fermiones con qiskit-fermions
En primer lugar, representaremos el hamiltoniano en un modelo de circuito fermiónico utilizando qiskit-fermions, que proporciona pasadas de transpilador y puertas específicas para circuitos fermiónicos. Estos se utilizarán posteriormente, antes de que se ejecuten las etapas tradicionales del transpilador de Qiskit para este flujo de trabajo.
Agrupación de términos
Para garantizar la reproducibilidad de los resultados, primero utilizamos canonical_order para ordenar los términos basándonos únicamente en su estructura. Por lo tanto, el orden de los operadores en la lista canon es fijo. Esto garantiza la reproducibilidad de los operadores creados, ya que la función QDriftTrotterization «pass» que utilizaremos en las próximas ejemplos selecciona índices aleatorios para crear los operadores « qDRIFT ».
En este paso, aprovechamos las numerosas simetrías presentes en el hamiltoniano de la estructura electrónica agrupando los términos relacionados que tienen coeficientes idénticos. Aunque al hacerlo se modifica la distribución de los coeficientes de los operadores a partir de la cual toma muestras el protocolo « qDRIFT », esto no afecta a sus garantías de convergencia. Es fundamental señalar que agrupar los términos relacionados por simetría da lugar a una cancelación favorable de los términos de Pauli y a una profundidad de circuito globalmente menor a la hora de hacer evolucionar un estado en el tiempo bajo su acción.
qiskit-fermions ofrece la función group_terms_by_electronic_structure que realiza esta agrupación por nosotros.
Ten en cuenta que la fórmula group_terms_by_electronic_structure parte de que los términos están ordenados normalmente.
Filtrado de términos diagonales
Eliminamos los términos diagonales del hamiltoniano utilizado para generar los circuitos, de modo que qDRIFT los intervalos de muestreo se dediquen a los términos que desplazan la población entre configuraciones. Lo mejor es eliminar esos términos del hamiltoniano en este momento, antes de construir la puerta Evolution en el siguiente paso.
Los términos en cuestión son aquellos que son diagonales en la base de números de ocupación, es decir, los productos de los operadores numéricos . Hay tres tipos de términos que se ajustan a esta descripción:
- el desplazamiento energético constante, producto de operadores de número cero, cuya evolución temporal contribuye únicamente con una fase global;
- los operadores de número individual , cuya evolución temporal se reduce a rotaciones de un solo qubit ;
- los productos de orden superior, como .
Por sí solas, ninguna de ellas provoca cambios en la distribución de la población entre las configuraciones de ocupación y número; solo actúan sobre las fases de las configuraciones ya existentes. Sin embargo, no son inertes: esas fases relativas influyen en la interferencia generada por los términos de excitación más adelante en el circuito, por lo que filtrarlas modifica la evolución que se genera realmente y puede alterar la distribución de muestreo. Se trata de una aproximación deliberada en la etapa de generación del circuito, realizada para centrar el muestreo en los términos de excitación, y no de una etapa que deje intacta la distribución muestreada. A diferencia de la agrupación de simetría anterior, que mantiene intactas las garantías de convergencia de « qDRIFT », este filtro modifica el operador que se somete a la evolución. Por lo tanto, los circuitos ya no se aproximan a la evolución bajo el hamiltoniano completo, y los límites de error de « qDRIFT » se aplican al operador filtrado en lugar de al original. Esto es aceptable en este caso porque los circuitos no son más que una heurística de muestreo utilizada para proponer configuraciones: no se pierde ningún término de la estimación de la energía en sí, ya que el filtro solo se aplica al hamiltoniano utilizado para construir los circuitos, mientras que la diagonalización clásica posterior utiliza el hamiltoniano completo, incluidos los términos diagonales. La precisión del SQD depende de ese paso clásico, que sigue siendo variacional en el subespacio muestreado, independientemente de cómo se hayan propuesto las configuraciones.
La función filter_diagonal_terms() elimina dichos términos de un operador sin modificarlo. Los identifica a partir de su estructura de orden normal —el multiconjunto de modos de creación coincide con el multiconjunto de modos de aniquilación—, por lo que solo es válido para un operador que ya tenga orden normal. Esta suposición no se comprueba en tiempo de ejecución.
# Apply automatic grouping
canon = canonical_order(hamiltonian.normal_ordered().simplify(atol=1e-16))
exit_code = group_terms_by_electronic_structure(
canon, num_modes, two_body_physicist_order=False
)
filter_diagonal_terms(canon)
print(len(canon.groups))5060Ahora que hemos agrupado los términos del hamiltoniano, vamos a fijar los siguientes parámetros para generar el conjunto de circuitos:
- El número de circuitos que hay que generar:
num_circuits - La longitud de cada circuito en términos de grupos de excitación:
num_exc - El factor que explica las diferentes duraciones de la evolución:
times
Creación de circuitos fermiónicos
Ahora crearemos circuitos fermiónicos para cada uno de los intervalos de tiempo. Cada circuito estará formado por una única puerta de evolución, con el tiempo de evolución que hemos establecido anteriormente. El operador de evolución es el hamiltoniano. Posteriormente, aplicamos varias pasadas del transpilador a estos circuitos para crear circuitos « qDRIFT ».
Preparación del Ansatz
Preparamos el estado de Hartree-Fock utilizando la clase InitializeModes . En el caso del nitrógeno, el proceso consiste simplemente en aplicar puertas X a los primeros num_elec_a qubits y, a continuación, a los qubits num_elec_b ; en ambos casos, el número es igual a siete para el nitrógeno. Este estado representa los siete electrones y los siete del nitrógeno.
# SqDRIFT parameters
times = [1.0, 10.0] # Total evolution times used for the subspace creation
num_exc = 10 # Number of excitation groups per circuit
num_circuits = 200 # Number of circuits to generate
init_circuits = []
hf_gate = InitializeModes.from_hartree_fock(norb, (num_elec_a, num_elec_b))
for time in times:
evo_gate = Evolution(num_modes, canon, time)
circ = FermionicCircuit(num_modes)
circ.append(hf_gate, circ.modes)
circ.append(evo_gate, circ.modes)
init_circuits.append(circ)Paso 2: Optimizar el problema para su ejecución en hardware cuántico
Ahora que ya tenemos nuestros circuitos, lo primero que haremos será utilizar las pasadas disponibles en qiskit-fermions para llevar a cabo optimizaciones a nivel fermiónico, y a continuación compilaremos nuestro circuito para el backend que elijamos. Dado que se trata de un experimento en simulador, lo haremos primero con el « AerSimulator ».
Cálculo del peso de cada grupo
En este paso, realizamos el muestreo « qDRIFT » de los términos de forma estocástica, con probabilidades proporcionales a sus coeficientes en el hamiltoniano. La etapa del transpilador « qDRIFT » se encarga de ello por nosotros. Ahora podemos crear circuitos menos profundos que se pueden ejecutar en el hardware de forma más eficiente a pesar de la conectividad limitada de los qubits, incluso cuando el hamiltoniano contiene acoplamientos de largo alcance y términos superiores a los cuadráticos. Tras agrupar los términos, realiza un muestreo de los operadores en función de sus pesos. Para cada operador , el peso se define de la siguiente manera:
Dado que los términos se agruparon en el paso 1, cada « » aquí corresponde a un grupo completo: « » es el coeficiente absoluto medio de los términos del grupo « », y cada término del grupo se desarrolla reduciendo su coeficiente a su signo.
Optimizaciones fermiónicas y nativas del hardware
La función devuelve generate_preset_jw_pass_manager() un objeto MultiStagePassManager que toma un y FermionicCircuit genera un circuito final optimizado que podemos transpilar para ejecutarlo en nuestro hardware. Sustituimos su etapa de optimización predeterminada por una que FermionicPassManager contenga nuestra pasada QDriftTrotterization :
- El paso
QDriftTrotterizationutiliza internamente el cálculo de ponderaciones y el muestreo para generar los circuitos que utilizaremos para el muestreo - Esta pasada
RelabelModeses otra pasada de optimización que se puede utilizar para permutar los modos fermiónicos con el fin de optimizar la conectividad entre los qubits y reducir la profundidad de las puertas; para más información, consulta la referencia de la API
Las etapas restantes se ejecutan MultiStagePassManager automáticamente y se encargan de la correspondencia completa entre fermiones y qubits:
- F2QLayout : El gestor de pasadas predefinido aplica la pasada
TrivialF2QLayout, que asigna de forma trivial los bits fermiónicos de a los qubits de . - F2QSynth : Una fase de transpilación para convertir las instrucciones de circuitos basados en fermiones en instrucciones basadas en qubits.
qdrift = QDriftTrotterization(num_exc, rng=19)
pm = generate_preset_jw_pass_manager()
pm.optimization = FermionicPassManager([qdrift])
sqdrift_circuits = []
for circ in init_circuits:
sqdrift_circuits += (pm.run(circ) for _ in range(num_circuits))
for circ in sqdrift_circuits:
circ.measure_all()
print(len(sqdrift_circuits))400Ahora que hemos terminado con las optimizaciones a nivel fermiónico, podemos transpilar los circuitos para ejecutarlos en el simulador.
simulator = AerSimulator()
shots = 100
transpiled_circuits = transpile(sqdrift_circuits, simulator)Paso 3: Ejecutar con el comando « Qiskit primitives »
Ahora que ya tenemos nuestros circuitos, podemos ejecutarlos utilizando Qiskit primitives en AerSimulator. Sumaremos todos los recuentos de los distintos circuitos. Los convertimos en vectores booleanos antes de someterlos finalmente a un posprocesamiento con SQD.
print(
f"Executing {len(transpiled_circuits)} circuits with {shots} shots each..."
)
job = simulator.run(transpiled_circuits, shots=shots)
result = job.result()
all_counts = [result.get_counts(i) for i in range(len(transpiled_circuits))]
print(len(all_counts), "length before post processing")Executing 400 circuits with 100 shots each...
400 length before post processingPaso 4: Realizar el posprocesamiento y obtener el resultado en el formato clásico deseado
Uso de cadenas de bits para SQD
Ahora podemos aplicar el método de diagonalización a las cadenas de bits seleccionadas para hallar el valor propio más bajo que se corresponderá con la energía del estado fundamental de la molécula. Creamos una función de devolución de llamada, declaramos las ocupaciones iniciales y configuramos los parámetros antes de ejecutar finalmente el esquema de diagonalización. La función de llamada de retorno se utiliza para mostrar la iteración actual y la estimación del valor propio actual en cada iteración.
Por último, para obtener la estimación del estado fundamental, sumamos el nuclear_repulsion_energy a la energía resultante.
Nota : La dimensión del subespacio no es fija entre iteraciones, ni siquiera en el simulador sin ruido: cada submuestra genera un conjunto diferente de configuraciones, y el paso de recuperación remodela el conjunto entre iteraciones, por lo que la dimensión indicada varía de una submuestra a otra. El muestreo silencioso no determina por sí solo la dimensión del subespacio seleccionado. Sin embargo, la ejecución en hardware tiende a generar subespacios sistemáticamente más grandes, ya que las tomas con ruido rompen la simetría del número de partículas y la recuperación de la configuración las convierte en vectores base adicionales. Por eso, también introduciremos otro paso para recortar las cadenas de bits en la sección dedicada al hardware.
combined_counts = {}
for counts in all_counts:
for bitstring, count in counts.items():
combined_counts[bitstring] = combined_counts.get(bitstring, 0) + count
bit_array = BitArray.from_counts(combined_counts)
print(bit_array.num_shots)
print(f" Alpha electrons: {num_elec_a}")
print(f" Beta electrons: {num_elec_b}")
print(f" Number of orbitals: {norb}")
print(f" Number of spin orbitals (qubits): {2*norb}")
print(f"Integral shapes: h1e={h1e.shape}, h2e={h2e.shape}")
# SQD parameters
samples_per_batch = 300
num_batches = 3
max_iterations = 5
initial_occupancies = (
np.array([1] * num_elec_a + [0] * (norb - num_elec_a)), # alpha
np.array([1] * num_elec_b + [0] * (norb - num_elec_b)), # beta
)
result_history = []
def callback(results: list[SCIResult]):
result_history.append(results)
iteration = len(result_history)
print(f"Iteration {iteration}")
for i, result in enumerate(results):
print(f"\tSubsample {i}")
print(f"\t\tEnergy: {result.energy + nuclear_repulsion_energy}")
print(
f"\t\tSubspace dimension: {np.prod(result.sci_state.amplitudes.shape)}"
)
# Run SQD with configuration recovery
print("\nRunning SQD with configuration recovery...")
result = diagonalize_fermionic_hamiltonian(
h1e,
h2e,
bit_array,
samples_per_batch=samples_per_batch,
norb=norb,
nelec=(num_elec_a, num_elec_b),
num_batches=num_batches,
energy_tol=1e-3,
occupancies_tol=1e-3,
max_iterations=max_iterations,
initial_occupancies=initial_occupancies,
seed=42,
callback=callback,
)
computed_energy = result.energy + nuclear_repulsion_energy
print("FINAL SQD RESULTS")
print(f"Orbital occupancies (alpha): {result.orbital_occupancies[0]}")
print(f"Orbital occupancies (beta): {result.orbital_occupancies[1]}")
energy_error = abs(computed_energy - reference_energy)
print(f"Reference Energy: {reference_energy:.10f} Ha")
print(f"Computed Energy: {computed_energy:.10f} Ha")
print(f"Error: {energy_error:.10e} Ha")40000
Alpha electrons: 7
Beta electrons: 7
Number of orbitals: 10
Number of spin orbitals (qubits): 20
Integral shapes: h1e=(10, 10), h2e=(10, 10, 10, 10)
Running SQD with configuration recovery...
Iteration 1
Subsample 0
Energy: -107.64767025226178
Subspace dimension: 5538
Subsample 1
Energy: -107.64772799119115
Subspace dimension: 5670
Subsample 2
Energy: -107.64765512281548
Subspace dimension: 5767
Iteration 2
Subsample 0
Energy: -107.64795948524682
Subspace dimension: 6080
Subsample 1
Energy: -107.64806617355072
Subspace dimension: 6300
Subsample 2
Energy: -107.64802260640258
Subspace dimension: 6308
FINAL SQD RESULTS
Orbital occupancies (alpha): [0.99999464 0.99999643 0.99584631 0.99332984 0.96684652 0.96686712
0.99301927 0.0373282 0.0373266 0.00944508]
Orbital occupancies (beta): [0.99999462 0.99999643 0.9958261 0.99332349 0.96684268 0.96686737
0.99302145 0.03733536 0.03733399 0.0094585 ]
Reference Energy: -107.6481842917 Ha
Computed Energy: -107.6480661736 Ha
Error: 1.1811817564e-04 HaEjemplo de hardware
En este ejemplo se utilizan 20 qubits (10 orbitales espaciales). Esa elección es una medida de comodidad para un tutorial que debe ejecutarse rápidamente, no un límite estricto del método.
El coste del paso clásico no viene determinado directamente por el número de qubits. SQD diagonaliza el hamiltoniano proyectado sobre el subespacio generado por las configuraciones muestreadas, por lo que lo que determina el coste clásico es la dimensión de ese subespacio seleccionado —que aquí viene determinada por samples_per_batch, num_batches, y el número de configuraciones distintas que producen realmente los circuitos— junto con el álgebra lineal dispersa necesaria para aplicar el hamiltoniano proyectado. El espacio CI completo crece de forma combinatoria con los orbitales y los electrones, pero el subespacio seleccionado es una pequeña parte ajustable del mismo, y controlamos su tamaño directamente. Por consiguiente, el número de qubits y la dificultad clásica pueden variar de forma relativamente independiente: un espacio orbital más amplio, muestreado en un subespacio modesto, puede resultar más económico que un sistema más pequeño diagonalizado sobre un espacio orbital muy amplio.
En la práctica, por lo tanto, el tamaño viable del sistema depende de la dimensión del subespacio que se necesite para alcanzar la precisión deseada, así como de la memoria y los núcleos de que disponga el solucionador de valores propios. Los espacios orbitales más amplios suelen requerir un subespacio mayor para alcanzar la precisión química, y eso es lo que, en última instancia, justifica el uso de recursos distribuidos; véase qiskit-addon-sqd-hpc para ampliar este paso. En lugar de fijar un límite rígido, lo más práctico es observar la dimensión del subespacio indicada y la convergencia de la energía a lo largo de las iteraciones, y aumentar el tamaño del subespacio hasta que la energía deje de mejorar o se agote la memoria disponible.
Nota: Debido al error de muestreo provocado por el ruido del hardware, el subespacio creado para la diagonalización en la ejecución en hardware será mayor que el que obtenemos al utilizar el simulador. Aunque aumenta la dimensión del subespacio que queremos diagonalizar, el procedimiento nos sigue proporcionando una respuesta precisa gracias a la robustez del SQD frente al ruido.
Eliminación de cadenas espurias
Aquí podemos optar por realizar un paso adicional. Cuando dispongamos de todas las cadenas de bits resultantes de las ejecuciones del circuito, podemos filtrar las cadenas de bits no válidas antes de ejecutar el SQD, o bien continuar sin realizar ninguna poda. Por lo general, es preferible omitir la poda en las ejecuciones de hardware, ya que así se mantienen disponibles los resultados con simetría rota para la recuperación de la configuración, lo que permite repararlos y convertirlos en configuraciones válidas, ampliando así el subespacio en lugar de descartar esos resultados directamente.
Dado que el nitrógeno solo puede tener siete y siete electrones, se pueden descartar todas aquellas cadenas de bits que tengan más o menos de siete 1s en la primera y la segunda mitad de la salida. Definimos una función que comprueba si las cadenas de bits son válidas y, en caso contrario, las descarta. Una vez que filtramos las cadenas de bits espurias, el resto se envía al esquema de diagonalización. Utiliza el indicador PRUNE que aparece a continuación para alternar entre los dos comportamientos.
Ten en cuenta que la poda es solo una de las varias opciones que determinan el subespacio final, junto con el número de circuitos, el conjunto de tiempos de evolución y el filtrado de términos diagonales. Comparar una ejecución podada con una no podada solo resulta informativo si el resto de factores se mantienen constantes; en la pestaña de C++ se aborda este tema con más detalle, ya que utiliza la poselección en lugar de la recuperación y también difiere en esos otros parámetros.
name = "fcidump_files/N2_sto_3g"
fcidump = tools.fcidump.read(name)
# Extract metadata from the FCIDump header
norb = fcidump["NORB"] # number of spatial orbitals
nelec = fcidump["NELEC"] # total number of electrons
e_nuc = fcidump["ECORE"] # nuclear repulsion / core energy
ms2 = fcidump["MS2"] # 2S (spin)
num_elec_a = (nelec + ms2) // 2 # alpha electrons
num_elec_b = (nelec - ms2) // 2 # beta electrons
# Reconstruct full 4-index ERIs from the FCIDump (stored in 8-fold symmetry)
h1e = fcidump["H1"] # shape (norb, norb)
h2e = ao2mo.restore( # shape (norb, norb, norb, norb)
1, fcidump["H2"], norb
)
cisolver = fci.direct_spin1.FCI()
cisolver.max_cycle = 200
cisolver.conv_tol = 1e-12
e_fci, _ = cisolver.kernel(
h1e,
h2e,
norb,
(num_elec_a, num_elec_b),
ecore=e_nuc, # adds nuclear repulsion to the final energy
)
reference_energy = e_fci
print(f"Reference FCI Energy = {reference_energy:.10f} Ha")
nuclear_repulsion_energy = fcidump["ECORE"]
print(f"Nuclear Repulsion Energy = {nuclear_repulsion_energy:.10f} Ha")
fcidump = FCIDump.from_file(name)
hamiltonian = FermionOperator.from_fcidump(fcidump)
num_modes = 2 * fcidump.norb
# Apply automatic grouping
canon = canonical_order(hamiltonian.normal_ordered().simplify(atol=1e-16))
exit_code = group_terms_by_electronic_structure(
canon, num_modes, two_body_physicist_order=False
)
filter_diagonal_terms(canon)
print(len(canon.groups))
# SqDRIFT parameters
times = [1.0, 10.0] # Total evolution times used for the subspace creation
num_exc = 10 # Number of excitation groups per circuit
num_circuits = 200 # Number of circuits to generate
init_circuits = []
hf_gate = InitializeModes.from_hartree_fock(norb, (num_elec_a, num_elec_b))
for time in times:
evo_gate = Evolution(num_modes, canon, time)
circ = FermionicCircuit(num_modes)
circ.append(hf_gate, circ.modes)
circ.append(evo_gate, circ.modes)
init_circuits.append(circ)
# Calculate weights for sampling (one per group)
qdrift = QDriftTrotterization(num_exc, rng=19)
pm = generate_preset_jw_pass_manager()
pm.optimization = FermionicPassManager([qdrift])
sqdrift_circuits = []
for circ in init_circuits:
sqdrift_circuits += (pm.run(circ) for _ in range(num_circuits))
for circ in sqdrift_circuits:
circ.measure_all()
print(len(sqdrift_circuits))
# This example assumes you have saved your IBM Quantum Platform account locally.
service = QiskitRuntimeService(channel="ibm_quantum_platform")
# Select backend (choose based on qubit requirements)
backend = service.least_busy(
operational=True,
simulator=False,
min_num_qubits=2 * norb,
)
print(f"Selected backend: {backend.name} ({backend.num_qubits} qubits)")
# Transpile for hardware
transpiled_circuits = transpile(
sqdrift_circuits,
backend=backend,
optimization_level=3,
seed_transpiler=42,
)
shots = 100
sampler = Sampler(mode=backend)
sampler.options.environment.job_tags = ["TUT_SQDRIFT"]
job = sampler.run(transpiled_circuits, shots=shots)
result = job.result()
# Extract counts from SamplerV2 results
all_counts = [pub_result.data.meas.get_counts() for pub_result in result]
# Set to True to filter out bitstrings that violate electron-number conservation
PRUNE = False
def is_valid_bitstring(
bitstring: str, norb: int, nelec: tuple[int, int]
) -> bool:
n_alpha, n_beta = nelec
return (
len(bitstring) == 2 * norb
and bitstring[norb:].count("1") == n_alpha
and bitstring[:norb].count("1") == n_beta
)
if PRUNE:
all_counts_filtered = []
for counts in all_counts:
filtered_count = {}
for key in counts:
if not is_valid_bitstring(key, norb, (num_elec_a, num_elec_b)):
continue
elif key not in filtered_count.keys():
filtered_count[key] = counts[key]
else:
filtered_count[key] += counts[key]
all_counts_filtered.append(filtered_count)
all_counts = all_counts_filtered
combined_counts = {}
for counts in all_counts:
for bitstring, count in counts.items():
combined_counts[bitstring] = combined_counts.get(bitstring, 0) + count
bit_array = BitArray.from_counts(combined_counts)
print(bit_array.num_shots)
print("Electron configuration:")
print(f" Total electrons: {nelec}")
print(f" Alpha electrons: {num_elec_a}")
print(f" Beta electrons: {num_elec_b}")
print(f" Number of orbitals: {norb}")
print(f" Number of spin orbitals (qubits): {2*norb}")
print(f"Integral shapes: h1e={h1e.shape}, h2e={h2e.shape}")
# SQD parameters
samples_per_batch = 300
num_batches = 3
max_iterations = 5
initial_occupancies = (
np.array([1] * num_elec_a + [0] * (norb - num_elec_a)), # alpha
np.array([1] * num_elec_b + [0] * (norb - num_elec_b)), # beta
)
result_history = []
def callback(results: list[SCIResult]):
result_history.append(results)
iteration = len(result_history)
print(f"Iteration {iteration}")
for i, result in enumerate(results):
print(f"\tSubsample {i}")
print(f"\t\tEnergy: {result.energy + nuclear_repulsion_energy}")
print(
f"\t\tSubspace dimension: {np.prod(result.sci_state.amplitudes.shape)}"
)
# Run SQD with configuration recovery
print("\nRunning SQD with configuration recovery...")
result = diagonalize_fermionic_hamiltonian(
h1e,
h2e,
bit_array,
samples_per_batch=samples_per_batch,
norb=norb,
nelec=(num_elec_a, num_elec_b),
num_batches=num_batches,
energy_tol=1e-3,
occupancies_tol=1e-3,
max_iterations=max_iterations,
initial_occupancies=initial_occupancies,
seed=42,
callback=callback,
)
computed_energy = result.energy + nuclear_repulsion_energy
print("FINAL SQD RESULTS")
print(f"Orbital occupancies (alpha): {result.orbital_occupancies[0]}")
print(f"Orbital occupancies (beta): {result.orbital_occupancies[1]}")
energy_error = abs(computed_energy - reference_energy)
print(f"Reference Energy: {reference_energy:.10f} Ha")
print(f"Computed Energy: {computed_energy:.10f} Ha")
print(f"Error: {energy_error:.10e} Ha")Reference FCI Energy = -107.6481842917 Ha
Nuclear Repulsion Energy = 23.7887003074 Ha
5060
400
Selected backend: ibm_aachen (156 qubits)
40000
Electron configuration:
Total electrons: 14
Alpha electrons: 7
Beta electrons: 7
Number of orbitals: 10
Number of spin orbitals (qubits): 20
Integral shapes: h1e=(10, 10), h2e=(10, 10, 10, 10)
Running SQD with configuration recovery...
Iteration 1
Subsample 0
Energy: -107.64593072647523
Subspace dimension: 7221
Subsample 1
Energy: -107.6458270048177
Subspace dimension: 7209
Subsample 2
Energy: -107.64007673117075
Subspace dimension: 7138
Iteration 2
Subsample 0
Energy: -107.64757372124944
Subspace dimension: 9009
Subsample 1
Energy: -107.64674060104392
Subspace dimension: 8245
Subsample 2
Energy: -107.64731360491942
Subspace dimension: 8178
Iteration 3
Subsample 0
Energy: -107.64765518770588
Subspace dimension: 8835
Subsample 1
Energy: -107.64767975712016
Subspace dimension: 8649
Subsample 2
Energy: -107.64761634415606
Subspace dimension: 8648
FINAL SQD RESULTS
Orbital occupancies (alpha): [0.99999504 0.9999964 0.99590318 0.9932359 0.96697158 0.96696295
0.99298797 0.03728154 0.03728186 0.00938359]
Orbital occupancies (beta): [0.9999946 0.99999641 0.99590413 0.99323077 0.96697361 0.96696174
0.99298424 0.03728121 0.03728169 0.00939159]
Reference Energy: -107.6481842917 Ha
Computed Energy: -107.6476797571 Ha
Error: 5.0453460619e-04 HaEl código fuente en C++, el proyecto CMake y las instrucciones de compilación se encuentran en el directorio docs/tutorials/assets/sqdrift/cpp del repositorio de documentación de Qiskit. El archivo de entrada de FCIDump se encuentra en el directorio fcidump_files contiguo. Clona el repositorio o descarga ambos directorios para compilar y ejecutar este tutorial por tu cuenta.
Estimación de tiempo de ejecución: 180 segundos en un procesador Heron r3 (NOTA: Se trata únicamente de una estimación. (El tiempo de ejecución puede variar.)
Resultados del aprendizaje (C++)
- Descubre cómo crear circuitos con menor profundidad en comparación con la técnica de Trotter
- Recorre un flujo de trabajo completo para la estimación del estado fundamental utilizando « qDRIFT » y SQD
- Descubre cómo utilizarlo
qiskit-fermionsjunto con otros complementos de Qiskit para implementar un flujo de trabajo de este tipo
Requisitos previos (C++)
- Lee la descripción general de la diagonalización cuántica basada en muestras (SQD)
- Lee la lección sobre la diagonalización cuántica de Krylov basada en muestras (SKQD)
Antecedentes (C++)
SqDRIFT es una variante del SKQD que sustituye la necesidad de elegir un ansatz a partir del cual muestrear cadenas de bits por un conjunto de circuitos de evolución temporal construidos directamente a partir del hamiltoniano objetivo. Esto se consigue mediante el submuestreo de operadores de evolución temporal más pequeños a partir del hamiltoniano, basándose en sus coeficientes, lo que se conoce como el método de « qDRIFT » de Trotterización.
Ambas implementaciones utilizan Qiskit Fermions para representar el hamiltoniano molecular y construir circuitos para el algoritmo « qDRIFT ». La implementación de « Python » utiliza pasadas del transpilador fermiónico, mientras que la implementación en C++ muestrea explícitamente los grupos y los asigna a operadores de qubits.
Supongamos que el hamiltoniano tiene la forma:
donde, sin pérdida de generalidad, se exige que y que el mayor valor propio de sea igual, en valor absoluto, a . Cualquier prefactor con signo o complejo se absorbe en , por lo que los coeficientes son pesos estrictamente positivos, mientras que los determinan la dirección de cada término. Aquí, es el número de términos (o, tras la agrupación, el número de grupos) del hamiltoniano; se trata de una propiedad del hamiltoniano y es distinto del número de operadores muestreados en un único circuito, que a continuación se denota c .
A continuación, el algoritmo « qDRIFT » realiza, para el tiempo objetivo , un operador , donde va desde y representa el circuito SqDRIFT, definido como:
Aquí, es el número de operadores muestreados por circuito y es el número de circuitos del conjunto. El producto se calcula sobre los sorteos de , no sobre todos los términos hamiltonianos de , y, dado que los términos se extraen con repetición, el mismo puede aparecer más de una vez en un único .
La cantidad:
es la norma « » de los coeficientes, por lo que cada uno de los pasos « » evoluciona durante el mismo tiempo « », independientemente del término que se haya extraído. La uniformidad del ángulo de paso es la característica distintiva de qDRIFT: : un coeficiente influye en el resultado en función de la frecuencia con la que se extrae su término, y no en función del ángulo de rotación de dicho término. Los índices se extraen de la distribución:
Por lo tanto, la serie « » es una secuencia aleatoria de índices de términos extraídos de esta distribución. Dado que los valores de « » son positivos y su suma es « », se trata de una distribución de probabilidad normalizada, y la esperanza del canal resultante a partir de los muestreos aleatorios se aproxima a la evolución bajo « », con un error que disminuye a medida que aumenta « ». Ten en cuenta que el error de aproximación depende de y no del número de términos .
(En el artículo « SqDRIFT » se denota el número de términos como « » y la longitud de la sucesión como « »; aquí utilizamos « » y « » para diferenciar claramente ambos conceptos.)
Este tutorial muestra cómo generar un conjunto de circuitos aleatorios de este tipo. Una vez creados estos circuitos, de forma similar a como se crea un subespacio de Krylov para diferentes operadores, tomamos muestras de cadenas de bits de varios de esos operadores con distintos parámetros temporales. Esto garantiza una mayor superposición entre los vectores del estado fundamental y las cadenas de bits muestreadas.
Requisitos (C++)
Esta implementación se ejecuta en un IBM Quantum® e y utiliza un ejecutable SBD externo para la diagonalización clásica. No incluye el ejemplo del simulador « Python ».
Sigue las instrucciones de compilación de C++ para instalar el compilador, CMake, Rust, Boost y las dependencias de Qiskit. Instala Rust con rustup : las dependencias de Qiskit especifican su propia versión de Rust, que rustup descarga automáticamente; sin embargo, es posible que una instalación de Rust realizada desde Homebrew o desde un gestor de paquetes de Linux sea demasiado antigua. El paso final de SBD requiere, además, MPI, OpenMP, y BLAS/LAPACK. Actualmente, esta implementación no es compatible con Windows.
Utiliza el código fuente completo en C++ y el proyecto CMake del repositorio. Conserva el directorio fcidump_files adyacente al descargar el ejemplo. Una vez descargado el repositorio de documentación, compílalo con los siguientes comandos. La primera compilación descarga y compila todas las dependencias y tarda varios minutos.
cd docs/tutorials/assets/sqdrift/cpp
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config ReleaseLa compilación guarda el ejecutable SqDRIFT en docs/tutorials/assets/sqdrift/cpp (la raíz del proyecto), no en build/. Ejecútalo desde ./SqDRIFT el directorio raíz del proyecto para que pueda encontrarlo ../fcidump_files/N2_sto_3g. El resto de comandos de esta pestaña también se ejecutan desde la raíz del proyecto, salvo que se indique lo contrario. Si se produce ./SqDRIFT un error por falta de una biblioteca compartida, sigue las instrucciones específicas de la plataforma sobre la ruta de las bibliotecas que figuran en la guía de compilación; en macOS,, el ejecutable suele funcionar sin ellas.
Configuración (C++)
Los fragmentos que aparecen a continuación son extractos del programa completo « SqDRIFT.cpp ». Compila y ejecuta ese archivo como un único programa.
Antes de ejecutar « SqDRIFT, », asegúrate de que tu token de API de « IBM Quantum » esté configurado para la plataforma « IBM Cloud ».
Si tienes instalado qiskit-ibm-runtime « Python », la forma más sencilla de crear un archivo de configuración con el formato correcto es:
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(channel="ibm_quantum_platform", token="YOUR_IBM_QUANTUM_TOKEN_HERE")Esto escribe ( Linux/macOS~/.qiskit/qiskit-ibm.json ) o %USERPROFILE%\.qiskit\qiskit-ibm.json (Windows) con la forma correcta. Si prefieres crear el archivo manualmente:
{
"default-ibm-quantum-platform": {
"channel": "ibm_quantum_platform",
"token": "YOUR_IBM_QUANTUM_TOKEN_HERE"
}
}Consigue tu clave API en IBM Quantum Platform.
Nota: Si falta el archivo de configuración o este tiene un formato incorrecto, se produce un unwrap() «panic» de Rust poco útil al iniciar el programa, en lugar de un error claro. Comprueba dos veces el nombre de la clave (default-ibm-quantum-platform) y el valor del canal (ibm_quantum_platform) si tienes algún problema.
Implementación (C++)
Paso 1: Asignar entradas clásicas a un problema cuántico (C++)
Cargar el hamiltoniano molecular
char filename[] = "../fcidump_files/N2_sto_3g";
QfFCIDump* fcidump = qf_fcidump_from_file(filename);
if (fcidump == nullptr) {
std::cerr << "Failed to load FCIDump!" << std::endl;
return 1;
}
uint32_t norb = qf_fcidump_norb(fcidump);
uint32_t nelec = qf_fcidump_nelec(fcidump);
uint32_t n_alpha = nelec / 2;
uint32_t n_beta = nelec - n_alpha;
uint32_t num_modes = 2 * norb;
uint32_t num_qubits = num_modes;
std::cout << "✓ Loaded N2 molecule (" << norb << " orbitals, "
<< num_qubits << " qubits, " << nelec << " electrons)" << std::endl;
Salida:
✓ Loaded N2 molecule (10 orbitals, 20 qubits, 14 electrons)
Carga el hamiltoniano de la molécula « » desde un archivo FCIDump. El número de qubits es el doble del número de orbitales (espín hacia arriba y espín hacia abajo).
Crear el hamiltoniano y ordenarlo según el orden normal
// Create and normal order Hamiltonian
QfFermionOperator* hamiltonian = qf_ferm_op_from_fcidump(fcidump);
QfFermionOperator* normal_ordered = qf_ferm_op_normal_ordered(hamiltonian, nullptr);
Convierte los datos de FCIDump en un operador fermiónico y aplica el orden normal para simplificar la estructura del operador.
Clasificación de los grupos según su estructura electrónica
// Group terms by electronic structure
QfExitCode exit_code = qf_ferm_op_group_terms_by_electronic_structure(
normal_ordered, num_modes, false
);
if (exit_code != QfExitCode_Success) {
std::cerr << "Failed to group terms!" << std::endl;
return 1;
}
uint32_t num_groups = qf_ferm_op_num_groups(normal_ordered);
std::cout << "Grouped into " << num_groups << " groups" << std::endl;
// Split into group operators
QfFermionOperator** group_ops = new QfFermionOperator*[num_groups];
qf_ferm_op_split_out_groups(normal_ordered, nullptr, 0, group_ops);
Salida:
Grouped into 1902 groups
Los grupos relacionaron los términos hamiltonianos con la estructura electrónica antes de construir sus circuitos de evolución.
Calcular las ponderaciones de muestreo, normalizarlas y asignarlas a los operadores de qubits
Aquí el orden es importante. La normalización reescala cada coeficiente a una magnitud unitaria in situ, por lo que primero hay que calcular los pesos de muestreo. Si primero se lleva a cabo la normalización, cada se lee exactamente como y 1.0 el peso de cada grupo se reduce a un simple recuento de sus términos; en ese caso, la distribución muestral vendría determinada por el tamaño del grupo y no por los coeficientes físicos.
El peso de cada grupo es el coeficiente absoluto medio de sus términos. La opción « Python » de la pestaña QDriftTrotterization «pass» utiliza el mismo peso para un hamiltoniano agrupado. Si se utilizara la suma, « » variaría en función del tamaño del grupo, lo que a su vez distorsionaría el tiempo de evolución « ».
// Calculate sampling weights BEFORE normalization.
std::vector<double> weights(num_groups);
double total_weight = 0.0;
for (uint32_t i = 0; i < num_groups; i++) {
QkComplex64* coeffs;
uint64_t num_terms;
qf_ferm_op_get_coeffs(group_ops[i], &coeffs, &num_terms);
double abs_sum = 0.0;
for (uint64_t j = 0; j < num_terms; j++) {
abs_sum += std::sqrt(coeffs[j].re * coeffs[j].re +
coeffs[j].im * coeffs[j].im);
}
weights[i] = (num_terms > 0) ? (abs_sum / static_cast<double>(num_terms)) : 0.0;
total_weight += weights[i];
}
std::cout << "Total weight (λ): " << total_weight << std::endl;
// Normalize the evolution operators term-by-term, now that the sampling
// weights above have already captured the original magnitudes.
for (uint32_t i = 0; i < num_groups; i++) {
QkComplex64* coeffs;
uint64_t num_terms;
qf_ferm_op_get_coeffs(group_ops[i], &coeffs, &num_terms);
for (uint64_t j = 0; j < num_terms; j++) {
const double magnitude = std::sqrt(coeffs[j].re * coeffs[j].re +
coeffs[j].im * coeffs[j].im);
if (magnitude > 0.0) {
coeffs[j].re /= magnitude;
coeffs[j].im /= magnitude;
}
}
}
QkObs** qubit_ops = new QkObs*[num_groups];
for (uint32_t i = 0; i < num_groups; i++) {
QfExitCode jw_exit = qf_ferm_op_jordan_wigner(group_ops[i], num_qubits, &qubit_ops[i]);
if (jw_exit != QfExitCode_Success) {
std::cerr << "Failed to map group " << i << std::endl;
return 1;
}
}
std::cout << "Mapped all " << num_groups << " normalized groups to qubit operators" << std::endl;
Salida:
Total weight (λ): 337.261
Mapped all 1902 normalized groups to qubit operators
El peso es el coeficiente absoluto medio de los términos del grupo , obtenido a partir de los coeficientes originales. A continuación, la normalización reescala cada término fermiónico a una magnitud unitaria antes de la transformación de Jordan-Wigner, conservando únicamente su fase y signo en el operador evolucionado. Esto concuerda con qDRIFT's, donde la evolución de los términos normalizados: las magnitudes de los coeficientes determinan las probabilidades de muestreo mediante , mientras que la evolución del circuito utiliza el término normalizado para que los coeficientes grandes no se cuenten dos veces.
Ejemplos de conjuntos de operadores
// SqDRIFT Sampling: Create operator sets
const int num_circuits = 100; // Number of circuits to create
const int ops_per_circuit = 10; // Operators per circuit
const double time_step = 1; // Time step for evolution
std::cout << "\n Generating " << num_circuits << " circuits with "
<< ops_per_circuit << " operators each..." << std::endl;
std::mt19937 gen(42);
std::discrete_distribution<> dist(weights.begin(), weights.end());
// Store sampled operator indices
std::vector<std::vector<int>> operator_sets(num_circuits);
for (int i = 0; i < num_circuits; i++) {
operator_sets[i].resize(ops_per_circuit);
for (int j = 0; j < ops_per_circuit; j++) {
operator_sets[i][j] = dist(gen);
}
}
Salida:
Generating 100 circuits with 10 operators each...
Genera los conjuntos de operadores para 100 circuitos aleatorios, cada uno de los cuales contiene 10 grupos extraídos de la distribución ponderada. El siguiente paso consiste en convertir estos conjuntos en el conjunto de circuitos « qDRIFT ».
Paso 2: Crear los circuitos de « qDRIFT » (C++)
En la versión en C++, este paso construye los circuitos a partir de los conjuntos de operadores muestreados. La transpilación para el backend de destino se lleva a cabo en el paso 3, justo antes de que se envíe cada circuito.
// Create Suzuki-Trotter circuits by composing all operators
std::vector<Qiskit::circuit::QuantumCircuit> circuits;
circuits.reserve(num_circuits);
const uint32_t trotter_order = 1;
const uint32_t trotter_reps = 1;
const bool preserve_order = false;
const bool insert_barriers = false;
// Gate name mapping is static — compute once before all loops
auto name_map = Qiskit::circuit::get_standard_gate_name_mapping();
for (int circuit_idx = 0; circuit_idx < num_circuits; circuit_idx++) {
// Create initial circuit with state preparation using C++ API
Qiskit::circuit::QuantumCircuit qc(num_qubits, num_qubits);
// State preparation: apply X gates to spin-up qubits 0..n_alpha-1
// and spin-down qubits norb..norb+n_beta-1 (derived from FCIDump NELEC)
for (uint32_t q = 0; q < n_alpha && q < num_qubits; q++) {
qc.x(q);
}
for (uint32_t q = norb; q < norb + n_beta && q < num_qubits; q++) {
qc.x(q);
}
// For each operator in this circuit's set
for (int op_idx = 0; op_idx < ops_per_circuit; op_idx++) {
int group_idx = operator_sets[circuit_idx][op_idx];
double evolution_time = (total_weight * time_step) / ops_per_circuit;
// Create Suzuki-Trotter evolution circuit
QkCircuit* evolution_circuit = qk_circuit_library_suzuki_trotter(
qubit_ops[group_idx], trotter_order, trotter_reps,
evolution_time, preserve_order, insert_barriers
);
if (evolution_circuit != nullptr) {
// Get number of instructions
uint32_t num_ops = qk_circuit_num_instructions(evolution_circuit);
// Manually append each instruction
for (uint32_t i = 0; i < num_ops; i++) {
QkCircuitInstruction inst;
qk_circuit_get_instruction(evolution_circuit, i, &inst);
// Prepare qubit vector
std::vector<uint32_t> qubits(inst.num_qubits);
for (uint32_t j = 0; j < inst.num_qubits; j++) {
qubits[j] = inst.qubits[j];
}
// Get operation kind
QkOperationKind kind = qk_circuit_instruction_kind(evolution_circuit, i);
// Get mutable pointer to circuit
QkCircuit* mutable_circuit = qc.get_rust_circuit().get();
// Append based on operation type
if (kind == QkOperationKind_Gate) {
// Convert gate name string to QkGate enum
std::string gate_name(inst.name);
auto gate_it = name_map.find(gate_name);
if (gate_it != name_map.end()) {
QkGate gate_enum = gate_it->second.gate_map();
qk_circuit_parameterized_gate(
mutable_circuit,
gate_enum, // Use QkGate enum, not string
qubits.data(),
inst.params
);
} else {
std::cerr << "Unknown gate in evolution circuit: " << gate_name << std::endl;
qk_circuit_instruction_clear(&inst);
qk_circuit_free(evolution_circuit);
return 1;
}
} else if (kind == QkOperationKind_Barrier) {
qk_circuit_barrier(
mutable_circuit,
qubits.data(),
inst.num_qubits
);
} else if (kind == QkOperationKind_Reset) {
qk_circuit_reset(
mutable_circuit,
qubits[0]
);
}
qk_circuit_instruction_clear(&inst);
}
qk_circuit_free(evolution_circuit);
}
}
circuits.push_back(qc);
// Display progress
if ((circuit_idx + 1) % 10 == 0) {
std::cout << " Created " << (circuit_idx + 1) << "/"
<< num_circuits << " circuits" << std::endl;
}
}
std::cout << "✓ Created all " << num_circuits << " Suzuki-Trotter circuits" << std::endl;
Salida:
Created 10/100 circuits
Created 20/100 circuits
Created 30/100 circuits
Created 40/100 circuits
Created 50/100 circuits
Created 60/100 circuits
Created 70/100 circuits
Created 80/100 circuits
Created 90/100 circuits
Created 100/100 circuits
✓ Created all 100 Suzuki-Trotter circuits
A continuación, el programa completo muestra las estadísticas del circuito y los índices de los operadores del circuito 0, que se omiten aquí.
Para cada operador muestreado, crea un circuito de evolución de Suzuki-Trotter con un tiempo de , donde es el número de operadores muestreados por circuito y es el paso de tiempo. Dado que cada grupo muestreado se normalizó término por término antes de la correspondencia, esta evolución solo aplica la fase y el signo de cada término durante la rotación; las magnitudes contribuyen únicamente a través de la distribución de muestreo. El estado inicial de Hartree-Fock se prepara aplicando puertas X a los qubits 0-6 y 10-16 (siete electrones en cada sector de espín). Las instrucciones de cada circuito de evolución se añaden manualmente al circuito principal mediante la API de C++.
Paso 3: Ejecutar utilizando « Qiskit primitives » (C++)
std::cout << "\n Connecting to IBM Quantum Cloud..." << std::endl;
// Initialize IBM Quantum Compute Service using qiskit-cpp
Qiskit::service::QiskitRuntimeService service;
std::cout << "✓ Connected to IBM Quantum" << std::endl;
// Get backend
const std::string backend_name = "ibm_fez";
auto backend = service.backend(backend_name);
if (backend.name().empty()) {
std::cerr << "Backend " << backend_name
<< " is not available to this account." << std::endl;
return 1;
}
std::cout << "✓ Selected backend: " << backend.name() << std::endl;
// Create sampler primitive
const int32_t shots = 100; // Number of shots per circuit
Qiskit::primitives::BackendSamplerV2 sampler(backend, shots);
std::cout << "\n Submitting " << num_circuits << " circuits ("
<< shots << " shots each)..." << std::endl;
// Submit one job per circuit.
std::vector<std::shared_ptr<Qiskit::primitives::BasePrimitiveJob>> jobs;
for (int i = 0; i < num_circuits; i++) {
for (uint32_t q = 0; q < num_qubits; q++) {
circuits[i].measure(q, q);
}
Qiskit::circuit::QuantumCircuit transpiled_qc =
Qiskit::compiler::transpile(circuits[i], backend, 2, 1.0, 42);
std::vector<Qiskit::primitives::SamplerPub> pubs;
pubs.push_back(Qiskit::primitives::SamplerPub(transpiled_qc, shots));
jobs.push_back(sampler.run(pubs));
}
std::cout << "\n⏳ Waiting for " << num_circuits << " jobs to complete..." << std::endl;
// Collect results — poll each job and extract bitstrings
std::vector<boost::dynamic_bitset<>> all_bitstrings;
for (int i = 0; i < num_circuits; i++) {
while (!jobs[i]->in_final_state()) {
sleep(5);
}
if (jobs[i]->done()) {
auto result = jobs[i]->result();
if (result.size() > 0) {
auto& pub_result = result[0];
auto& bit_array = pub_result.data();
for (size_t s = 0; s < bit_array.num_shots(); s++) {
auto sample = bit_array[s].to_string();
boost::dynamic_bitset<> bs(num_qubits);
for (uint32_t bit = 0; bit < num_qubits; bit++) {
bs[bit] = (sample[num_qubits - 1 - bit] == '1');
}
all_bitstrings.push_back(bs);
}
}
} else {
std::cout << " Circuit " << (i+1) << " did not complete successfully" << std::endl;
}
}
std::cout << "\n✓ Collected " << all_bitstrings.size()
<< " total bitstrings from all circuits" << std::endl;
Se conecta a IBM Quantum, selecciona el backend por su nombre, compila cada circuito para ese backend y envía un trabajo por cada circuito : 100 trabajos Sampler independientes de 100 disparos cada uno en un registro de 20 qubits. A continuación, comprueba el estado de cada tarea hasta que finaliza y recopila las cadenas de bits de result[0]. Cada trabajo utiliza tiempo de la QPU y espera en la cola por separado, por lo que cabe esperar que este paso tarde mucho más que un único trabajo procesado por lotes.
Los resultados de hardware que se muestran en esta página proceden de una ejecución de ibm_pittsburgh (100 circuitos × 100 disparos), en la que se ha modificado backend_name el valor predeterminado de ibm_fez. Las muestras guardadas se sometieron a un procesamiento posterior utilizando el orden de bits del determinante corregido que se muestra en el paso 4. El ruido del hardware varía de una ejecución a otra, por lo que los recuentos y la energía final serán diferentes.
Salida:
Connecting to IBM Quantum Cloud...
✓ Connected to IBM Quantum
✓ Selected backend: ibm_pittsburgh
Submitting 100 circuits (100 shots each)...
⏳ Waiting for 100 jobs to complete...
✓ Collected 10000 total bitstrings from all circuits
Cambiar el backend: Cambia backend_name en SqDRIFT.cpp para seleccionar un dispositivo diferente; service.backends() muestra a qué dispositivos tiene acceso tu cuenta.
Paso 4: Realizar el posprocesamiento y devolver el resultado en el formato clásico deseado (C++)
Publicar cadenas de bits seleccionadas y escribir cadenas de CI
// Postselect bitstrings by Hamming weight (derived from FCIDump NELEC)
std::vector<double> bitstring_weights(all_bitstrings.size(), 1.0);
auto [filtered_bitstrings, filtered_weights] = Qiskit::addon::sqd::postselect_bitstrings(
all_bitstrings,
bitstring_weights,
Qiskit::addon::sqd::MatchesRightLeftHamming<uint32_t>(n_alpha, n_beta)
);
std::ignore = filtered_weights;
std::cout << "✓ Postselected " << filtered_bitstrings.size()
<< " bitstrings with Hamming weight ("
<< n_alpha << "," << n_beta << ")" << std::endl;
// Convert bitstrings to CI strings using SQD addon
auto ci_strings = Qiskit::addon::sqd::bitstrings_to_ci_strings_symmetrize_spin(
filtered_bitstrings,
std::nullopt // No dimension limit
);
std::cout << "✓ Generated " << ci_strings.size() << " CI strings" << std::endl;
// Write CI strings to file for SBD
std::ofstream alpha_file("alphadets_from_sqd.txt");
for (const auto& ci_string : ci_strings) {
std::string bitstr;
// SBD expects the highest orbital index first (bit zero on the right).
boost::to_string(ci_string, bitstr);
alpha_file << bitstr << "\n";
}
alpha_file.close();
std::cout << "✓ Wrote " << ci_strings.size()
<< " CI strings to alphadets_from_sqd.txt" << std::endl;
std::cout << " Ready for SBD diagonalization!" << std::endl;
Salida:
✓ Postselected 1137 bitstrings with Hamming weight (7,7)
✓ Generated 49 CI strings
✓ Wrote 49 CI strings to alphadets_from_sqd.txt
Ready for SBD diagonalization!
Selecciona cadenas de bits con el peso de Hamming correcto (n_alpha electrones con espín hacia arriba, n_beta electrones con espín hacia abajo, derivados del campo NELEC FCIDump), las convierte en cadenas de interacción de configuración (CI) mediante simetrización de espín y las escribe en alphadets_from_sqd.txt para su posterior diagonalización en bases seleccionadas.
Recursos de limpieza
// Cleanup
// circuits vector will be automatically cleaned up (RAII)
// Free qubit operators
for (uint32_t i = 0; i < num_groups; i++) {
qk_obs_free(qubit_ops[i]);
}
delete[] qubit_ops;
// Free fermionic operators
for (uint32_t i = 0; i < num_groups; i++) {
qf_ferm_op_free(group_ops[i]);
}
delete[] group_ops;
qf_ferm_op_free(normal_ordered);
qf_ferm_op_free(hamiltonian);
qf_fcidump_free(fcidump);
return 0;
Libera correctamente toda la memoria y los recursos asignados. El código C++ std::vector<QuantumCircuit> se libera automáticamente mediante RAII. Libera manualmente los operadores de qubits, los operadores fermiónicos y los datos de FCIDump.
Ahora que hemos creado la base para proyectar nuestro hamiltoniano, podemos continuar con el proceso de diagonalización para obtener la estimación del estado fundamental. Ejecuta los siguientes comandos desde el directorio raíz del proyecto, docs/tutorials/assets/sqdrift/cpp.
Compilar el binario diag SBD
CMake descarga SBD solo en formato fuente ( deps/sbd/ la compilación principal omite deliberadamente su compilación). La aplicación SBD utiliza un plain Makefile, y la propia SBD requiere MPI, OpenMP, y BLAS/LAPACK, por lo que plain no es make suficiente hasta que se hayan instalado dichos componentes y el archivo Configuration plain de la aplicación se ajuste a tu cadena de herramientas.
El programa lee Makefile los parámetros del compilador y del enlazador del archivo Configuration situado en el mismo directorio. La versión enviada al proyecto original está optimizada para una disposición específica de macOS/Homebrew y no se compila tal cual en un equipo de serie; en particular, no -I/opt/homebrew/opt/llvm/include contiene omp.h (se encuentra en un directorio de recursos de Clang específico de la versión), y Apple la rechaza clang++ de plano -fopenmp :
clang++: error: unsupported option '-fopenmp'
Se ha comprobado que la siguiente receta funciona en macOS (Apple Silicon, Darwin 25.6, Open MPI 5.x + Homebrew LLVM).
macOS (Apple Silicon) — probado
# 1. Install MPI and an OpenMP-capable compiler.
# Apple clang cannot do OpenMP, so Homebrew's LLVM provides both
# clang++ and libomp. BLAS/LAPACK come from Apple's Accelerate
# framework, which is already part of macOS — no openblas needed.
brew install open-mpi llvmA continuación, sustituye por deps/sbd/apps/chemistry_tpb_selected_basis_diagonalization/Configuration :
# Path to the SBD library
SBD_PATH=../..
# MPI C++ compiler. OMPI_CXX points Open MPI's wrapper at Homebrew's clang++,
# which (unlike Apple clang) supports -fopenmp.
CCCOM=OMPI_CXX=/opt/homebrew/opt/llvm/bin/clang++ mpicxx
# Build flags. omp.h lives in a version-specific clang resource dir, so let
# Homebrew's clang++ find it itself rather than hardcoding an -I path.
CCFLAGS= -std=c++17 -fopenmp -O3
# Link flags: OpenMP runtime from Homebrew LLVM, BLAS/LAPACK from the
# Accelerate framework that ships with macOS (no openblas install needed).
SYSLIB= -L/opt/homebrew/opt/llvm/lib -lomp -framework Accelerate
# 2. Build
cd deps/sbd/apps/chemistry_tpb_selected_basis_diagonalization
make
cd ../../../..Si la compilación se realiza correctamente, se muestran las dos líneas de compilación y enlace y se genera un archivo binario diag junto al Makefile. Uno ld: warning: ignoring duplicate libraries: '-lomp' es inofensivo.
El prefijo de Intel macOS: Homebrew es /usr/local en lugar de /opt/homebrew, así que sustitúyelo en ambas rutas anteriores.
Linux ( Ubuntu / Debian ) — sin comprobar
Los siguientes requisitos previos se refieren a MPI, OpenMP, y BLAS/LAPACK. GCC lo admite de forma nativa -fopenmp , por lo que no es necesario modificar la configuración del compilador. Esto no se ha verificado en Linux :
sudo apt install -y libopenmpi-dev libomp-dev libblas-dev liblapack-devSBD_PATH=../..
CCCOM=mpicxx
CCFLAGS= -std=c++17 -fopenmp -O3
SYSLIB= -llapack -lblas
Ejecuta « SqDRIFT » para generar cadenas de CI
Si ya has seguido los pasos del ./SqDRIFT 1 al 4, ya alphadets_from_sqd.txt existe en la raíz del proyecto y puedes saltarte este paso. De lo contrario, ejecútalo desde el directorio raíz del proyecto:
./SqDRIFT
# Writes alphadets_from_sqd.txt in the project rootRealiza la diagonalización
cd deps/sbd/apps/chemistry_tpb_selected_basis_diagonalization
# The FCIDump lives in the shared sqdrift/fcidump_files/ directory, one level
# above this tutorial's project root, hence five levels up from here.
ln -sf ../../../../../fcidump_files/N2_sto_3g fcidump.txt
# alphadets_from_sqd.txt is written by ./SqDRIFT into the project root itself,
# which is four levels up.
ln -sf ../../../../alphadets_from_sqd.txt alphadets.txt
./diag \
--fcidump fcidump.txt \
--adetfile alphadets.txt \
--method 0 \
--iteration 100 \
--block 10 \
--tolerance 1e-8 \
| tee ../../../../sbd_output.txt
cd ../../../..Parámetros:
--fcidump: Ruta al archivo FCIDump que contiene el hamiltoniano--adetfile: Ruta al archivo de determinantes alfa (cadenas CI de SqDRIFT )--method 0: Método de diagonalización de Davidson--iteration 100: Número máximo de iteraciones de Davidson--block 10: Tamaño del bloque para el algoritmo de Davidson--tolerance 1e-8: Tolerancia de convergencia para la energía
Elapsed time for helper construction 0.001512 (sec)
Elapsed time for init 1e-06 (sec)
Davidson iteration 0.0 (tol=0.1857493937033264): -107.493531425221
Davidson iteration 0.1 (tol=0.0455699976495391): -107.5098150493379 -105.93368397386
Davidson iteration 0.2 (tol=0.009291923190816917): -107.5108356028703 -106.1245977311231 -105.6823366883346
Davidson iteration 0.3 (tol=0.001488674426419356): -107.5108711084406 -106.3219246016818 -105.7376912083761 -105.4392974334491
Davidson iteration 0.4 (tol=0.0003188697654737439): -107.5108722237947 -106.5935152393201 -106.1108017566682 -105.4932621297472
Davidson iteration 0.5 (tol=3.080273116687244e-05): -107.5108722587463 -106.7245930484005 -106.2313070085243 -105.6423237790271
Davidson iteration 0.6 (tol=2.981303155550973e-06): -107.5108722590151 -106.8008116251279 -106.330411722324 -105.69489630447
Davidson iteration 0.7 (tol=4.565075388900609e-07): -107.5108722590186 -106.8090569201597 -106.3345525819528 -106.0578325555555
Davidson iteration 0.8 (tol=4.870689101320012e-08): -107.5108722590186 -106.8098939189322 -106.3973756614146 -106.1617813840531
Davidson iteration 0.9 (tol=6.224330513865847e-09): -107.5108722590187 -106.8159869231416 -106.4315891597535 -106.2222021964013
Elapsed time for davidson 0.031276 (sec)
Elapsed time for diagonalization 0.031286 (sec)
Elapsed time for mult 0.002511 (sec)
Energy = -107.5108722590187
Elapsed time for measurement 4.6e-05 (sec)
Sample-based diagonalization: Energy = -107.5108722590187
Sample-based diagonalization: density = [1.999989340109303,1.99999662423008,1.999199121034164,1.981735389885314,1.999825046204946,1.999847567448985,1.995347419300126,0.01136512984757364,0.009844780833945199,0.002849581105572152
Sample-based diagonalization: carryover bitstrings = [], size = 0
La línea Energy = -107.5108722590187 representa la estimación del estado fundamental en el subespacio muestreado.
En qué se diferencia esto del ejemplo de « Python »
Los dos ejemplos difieren en varios aspectos a la vez, no solo en cómo tratan las cadenas de bits con simetría defectuosa, por lo que no cabe esperar que produzcan energías idénticas:
Columna « 1 » | Pestaña C++ | Python pestaña |
|---|---|---|
| Cadenas de bits con simetría defectuosa | Descartado por postselect_bitstrings en «Peso de Hamming» | Reparado mediante recuperación iterativa de la configuración |
| Estructura de bucle | Una sola pasada: muestra → poselección → diagonalización única | Bucle exterior: diagonalizar, leer las ocupaciones orbitales, recuperar las configuraciones, volver a diagonalizar |
| Número de circuitos | 100 circuitos (num_circuits = 100) | 400 vueltas (200 por tiempo de evolución) |
| Tiempos de evolución | Un único (time_step = 1) | Dos veces, times = [1.0, 10.0] |
| Términos diagonales | Se mantiene en el operador muestreado | Eliminado por filter_diagonal_terms, modificando la distribución muestral y los circuitos |
| Orden de los términos | Solo agrupados | canonical_order aplicado antes de la agrupación para garantizar la reproducibilidad |
| Resolutor de diagonalización | SBD (diag, external MPI/OpenMP binary) | qiskit-addon-sqd solucionador en tiempo de ejecución |
| Programa de fondo | Corregido por su nombre (backend_name = "ibm_fez"; en la ejecución de esta página se utilizó ibm_pittsburgh) | least_busy backend operativo con suficientes qubits |
| Transpilación | Cada circuito por separado, nivel de optimización 2 | Todos los circuitos en conjunto, nivel de optimización 3 |
| Envío de trabajos | 100 puestos de trabajo, uno por circuito | Un trabajo de Sampler que contiene los 400 circuitos |
| Simulación | Ninguno; solo hardware | Ejemplo de simulador sin ruido antes de la ejecución en hardware |
Ambas versiones utilizan el mismo hamiltoniano, agrupan sus términos según la estructura electrónica, ponderan cada grupo por el coeficiente absoluto medio de sus términos, parten del estado de Hartree-Fock y muestrean 10 grupos y 100 iteraciones por circuito.
Cada diferencia afecta al subespacio muestreado —y, por lo tanto, a la energía— de forma independiente. Tanto el conjunto más amplio como el segundo tiempo de evolución, más prolongado, amplían las configuraciones que explora el ejemplo de « Python »; el muestreo en más de un momento de tiempo confiere a « SqDRIFT » su cobertura, similar a la de Krylov, del espacio de baja energía. Al filtrar los términos diagonales, cambian los operadores que el muestreador puede seleccionar y sus ponderaciones, por lo que las dos ejecuciones muestrean distribuciones diferentes.
La poselección es un filtro estricto, y cualquier toma que no tenga el número correcto de partículas se descarta directamente. En la ejecución anterior, en la que se descartó la mayor parte de los datos, 1.137 de las 10.000 tomas superaron la comprobación de peso de Hamming (7,7) (aproximadamente el 11 %), reduciéndose a tan solo 49 determinantes distintos de . En cambio, la recuperación de la configuración utiliza las ocupaciones orbitales medias obtenidas de una diagonalización previa para invertir los bits y reparar esos «disparos», reciclando así lo que la postselección descartaría. Esa retroalimentación hace que sea intrínsecamente iterativo: recover_configurations in qiskit-addon-sqd-hpc requiere un argumento avg_occupancies que solo existe una vez que se ha ejecutado una diagonalización.
Esta ejecución en C++ da como resultado un subespacio de 49 determinantes de y una energía de Ha -107.5109 . El espacio completo de determinantes de para siete electrones en 10 orbitales consta únicamente de cadenas, por lo que al introducir las 120 en el mismo binario diag se obtiene la energía exacta de FCI, Ha -107.6482 . Por lo tanto, la serie se sitúa aproximadamente a Ha 0.1373 por encima del valor exacto, al tiempo que abarca 49 de los 120 determinantes de . Dado que el SQD es variacional en el subespacio seleccionado, un subespacio más pequeño solo puede aumentar la energía, por lo que el subespacio incompleto es la interpretación más directa de la diferencia.
Esta comprobación exacta de FCI solo es posible porque el ejemplo es pequeño; no forma parte del flujo de trabajo de SqDRIFT y, para tamaños de sistema en los que realmente se necesita SQD, no existe tal referencia. Esto solo sirve para cuantificar cuánto espacio se ha recuperado con esta ejecución.
La comparación con el ejemplo de « Python » no está controlada, por lo que no se debe atribuir ninguna diferencia únicamente a la ausencia de recuperación de la configuración; cabe señalar que el número de circuitos, el tiempo de evolución único, el filtrado de términos diagonales y el ruido del hardware difieren todos a la vez y todos ellos determinan la forma del subespacio.
Para subsanar esta carencia, aquí tienes algunos pasos que puedes seguir una vez finalizado este tutorial: aumentar shots, num_circuits, o ops_per_circuit; tomar muestras en más de un time_step; y añadir un bucle de recuperación encima de Qiskit::addon::sqd::recover_configurations.
Próximos pasos
- Diagonalización cuántica de Krylov basada en muestras de un modelo de red fermiónica : un tutorial relacionado que utiliza circuitos de evolución temporal en lugar de un enfoque variacional.
- Diagonalización cuántica basada en muestras de un hamiltoniano químico : un tutorial sobre cómo construir un circuito Jastrow de clúster unitario local (LUCJ) para la simulación de química cuántica.
- El artículo « SqDRIFT » —la obra en la que se basa este tutorial—. (Ten en cuenta que algunas de las optimizaciones que se tratan en este artículo se encuentran actualmente en fase de desarrollo, y que este tutorial está sujeto a cambios en el futuro en función de la evolución de las bibliotecas utilizadas.)