Saltar al contenido principal

Primeros pasos con las fórmulas multiproducto (MPF)

Primeros pasos con las fórmulas multiproducto (MPF)

Versiones de paquetes

El código de esta página fue desarrollado con los siguientes requisitos. Recomendamos usar estas versiones o versiones más recientes.

qiskit[all]~=2.3.0
qiskit-addon-utils~=0.3.0
qiskit-addon-mpf~=0.3.0
scipy~=1.16.3

Esta guía demuestra cómo usar el paquete qiskit-addon-mpf, utilizando la evolución temporal de un modelo de Ising como ejemplo. Con este paquete, puedes construir una Fórmula Multiproducto (MPF) que puede lograr un menor error de Trotter en las mediciones de observables. Las herramientas proporcionadas te permiten determinar los pesos de una MPF elegida, que luego se pueden usar para recombinar los valores de expectación estimados de varios circuitos de evolución temporal, cada uno con un número diferente de pasos de Trotter.

Comienza considerando el Hamiltoniano de un modelo de Ising con 10 sitios:

HIsing=i=19Ji,(i+1)ZiZi+1+i=110hiXiH_{\text{Ising}} = \sum_{i=1}^9 J_{i,(i+1)}Z_iZ_{i+1} + \sum_{i=1}^{10} h_i X_i

donde Ji,(i+1)J_{i,(i+1)} es la intensidad del acoplamiento y hih_i es la intensidad del campo magnético externo. Para plantear el problema, el observable a medir será la magnetización total del sistema

M=i=110Zi.\langle M \rangle = \sum_{i=1}^{10} \langle Z_i \rangle.

El fragmento de código a continuación prepara el Hamiltoniano de la cadena de Ising usando el paquete qiskit-addon-utils y define el observable que se medirá.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-mpf qiskit-addon-utils scipy
from qiskit.transpiler import CouplingMap
from qiskit.synthesis import SuzukiTrotter
from qiskit.quantum_info import SparsePauliOp
from qiskit.primitives import StatevectorEstimator
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_addon_utils.problem_generators import (
generate_xyz_hamiltonian,
generate_time_evolution_circuit,
)
from qiskit_addon_mpf.costs import (
setup_exact_problem,
setup_sum_of_squares_problem,
)
from qiskit_addon_mpf.static import setup_static_lse

from scipy.linalg import expm
import numpy as np

# Generate some coupling map to use for this example
coupling_map = CouplingMap.from_line(10, bidirectional=False)

# Get a qubit operator describing the Ising field model
hamiltonian = generate_xyz_hamiltonian(
coupling_map,
coupling_constants=(0.0, 0.0, 1.0),
ext_magnetic_field=(0.4, 0.0, 0.0),
)
print(f"Hamiltonian:\n {hamiltonian}\n")

L = coupling_map.size()
observable = SparsePauliOp.from_sparse_list(
[("Z", [i], 1 / L / 2) for i in range(L)], num_qubits=L
)
print(f"Observable:\n {observable}")
Hamiltonian:
SparsePauliOp(['IIIIIIIZZI', 'IIIIIZZIII', 'IIIZZIIIII', 'IZZIIIIIII', 'IIIIIIIIZZ', 'IIIIIIZZII', 'IIIIZZIIII', 'IIZZIIIIII', 'ZZIIIIIIII', 'IIIIIIIIIX', 'IIIIIIIIXI', 'IIIIIIIXII', 'IIIIIIXIII', 'IIIIIXIIII', 'IIIIXIIIII', 'IIIXIIIIII', 'IIXIIIIIII', 'IXIIIIIIII', 'XIIIIIIIII'],
coeffs=[1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j,
1. +0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j,
0.4+0.j, 0.4+0.j, 0.4+0.j])

Observable:
SparsePauliOp(['IIIIIIIIIZ', 'IIIIIIIIZI', 'IIIIIIIZII', 'IIIIIIZIII', 'IIIIIZIIII', 'IIIIZIIIII', 'IIIZIIIIII', 'IIZIIIIIII', 'IZIIIIIIII', 'ZIIIIIIIII'],
coeffs=[0.05+0.j, 0.05+0.j, 0.05+0.j, 0.05+0.j, 0.05+0.j, 0.05+0.j, 0.05+0.j,
0.05+0.j, 0.05+0.j, 0.05+0.j])

A continuación, prepara la MPF. La primera decisión es si los coeficientes serán estáticos (independientes del tiempo) o dinámicos; este tutorial usa una MPF estática. La siguiente decisión es el conjunto de valores kjk_j. Esto determina cuántos términos tendrá la MPF, así como cuántos pasos de Trotter usa cada término para simular la evolución temporal. La elección de los valores kjk_j es heurística, por lo que debes obtener tu propio conjunto de valores kjk_j "buenos". Lee las pautas para encontrar un buen conjunto de valores al final de la página de primeros pasos.

Luego, una vez determinados los valores kjk_j, puedes preparar el sistema de ecuaciones, Ax=bAx=b, a resolver. La matriz AA también está determinada por la fórmula de producto a usar. Las opciones aquí son su orden, que se establece en 22 en este ejemplo, y si la fórmula de producto debe ser simétrica o no, lo cual se establece en True para este ejemplo. El fragmento de código a continuación selecciona un tiempo total para evolucionar el sistema, los valores kjk_j a usar y el conjunto de ecuaciones a resolver usando el método qiskit_addon_mpf.static.setup_static_lse.

time = 8.0
trotter_steps = (8, 12, 19)

lse = setup_static_lse(trotter_steps, order=2, symmetric=True)
print(lse)
LSE(A=array([[1.00000000e+00, 1.00000000e+00, 1.00000000e+00],
[1.56250000e-02, 6.94444444e-03, 2.77008310e-03],
[2.44140625e-04, 4.82253086e-05, 7.67336039e-06]]), b=array([1., 0., 0.]))

Una vez instanciado el sistema lineal de ecuaciones, se puede resolver exactamente o mediante un modelo aproximado usando una suma de cuadrados (o la norma de Frobenius para coeficientes dinámicos; consulta la referencia de la API para más información). La decisión de usar un modelo aproximado suele surgir cuando la norma de los coeficientes para el conjunto de valores kjk_j elegido se considera demasiado alta y no es posible elegir un conjunto diferente de valores kjk_j. Esta guía demuestra ambos enfoques para comparar los resultados.

model_exact, coeffs_exact = setup_exact_problem(lse)
model_approx, coeffs_approx = setup_sum_of_squares_problem(
lse, max_l1_norm=3.0
)
model_exact.solve()
model_approx.solve()
print(f"Exact solution: {coeffs_exact.value}")
print(f"Approximate solution: {coeffs_approx.value}")
Exact solution: [ 0.17239057 -1.19447005  2.02207947]
Approximate solution: [-0.40454257 0.57553173 0.8290123 ]
nota

El objeto LSE también posee un método LSE.solve(), que resolverá el sistema de ecuaciones de forma exacta. La razón por la que se usa setup_exact_problem() en esta guía es para demostrar la interfaz que ofrecen los demás métodos aproximados.

Configurar y ejecutar los circuitos de Trotter

Ahora que se han obtenido los coeficientes xjx_j, el último paso es generar los circuitos de evolución temporal para el orden y el conjunto de pasos kjk_j elegidos de la MPF. El paquete qiskit-addon-utils puede acelerar este proceso.

circuits = []
for k in trotter_steps:
circ = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(order=2, reps=k),
time=time,
)
circuits.append(circ)
circuits[0].draw("mpl", fold=-1)

Salida de la celda de código anterior

circuits[1].draw("mpl", fold=-1)

Salida de la celda de código anterior

circuits[2].draw("mpl", fold=-1)

Salida de la celda de código anterior

Una vez construidos estos circuitos, puedes transpilarlos y ejecutarlos usando una QPU. Para este ejemplo, usaremos uno de los simuladores sin ruido para demostrar cómo se reduce el error de Trotter.

backend = GenericBackendV2(num_qubits=10)
transpiler = generate_preset_pass_manager(
optimization_level=2, backend=backend
)

transpiled_circuits = [transpiler.run(circ) for circ in circuits]

estimator = StatevectorEstimator()
job = estimator.run([(circ, observable) for circ in transpiled_circuits])
result = job.result()

mpf_evs = [res.data.evs for res in result]
print(mpf_evs)
[array(0.23799162), array(0.35754312), array(0.38649906)]

Reconstruir los resultados

Ahora que los circuitos se han ejecutado, reconstruir los resultados es bastante sencillo. Como se menciona en la página de descripción general de MPF, nuestro observable se reconstruye mediante la suma ponderada

M=jxjMj.\langle M \rangle = \sum_j x_j \langle M_j \rangle.

donde xjx_j son los coeficientes que encontramos y Mj\langle M_j \rangle es la estimación del observable iZi\sum_i \langle Z_i \rangle para cada circuito. Luego podemos comparar los resultados obtenidos con el valor exacto usando el paquete scipy.linalg.

exp_H = expm(-1j * time * hamiltonian.to_matrix())
initial_state = np.zeros(exp_H.shape[0])
initial_state[0] = 1.0

time_evolved_state = exp_H @ initial_state
exact_obs = (
time_evolved_state.conj() @ observable.to_matrix() @ time_evolved_state
)

# Print out the different observable measurements
print(f"Exact value: {exact_obs.real}")
print(f"PF with 19 steps: {mpf_evs[-1]}")
print(f"MPF using exact solution: {mpf_evs @ coeffs_exact.value}")
print(f"MPF using approximate solution: {mpf_evs @ coeffs_approx.value}")
Exact value: 0.4006024248789992
PF with 19 steps: 0.3864990619977402
MPF using exact solution: 0.3954847855979902
MPF using approximate solution: 0.4299121425348959

Aquí puedes ver que la MPF ha reducido el error de Trotter en comparación con el obtenido con una sola PF con kj=19k_j=19. Sin embargo, el modelo aproximado produjo un valor de expectación peor que el modelo exacto. Esto demuestra la importancia de usar criterios de convergencia estrictos en el modelo aproximado y de encontrar un conjunto "bueno" de valores kjk_j.