Vai al contenuto principale

Primi passi con le formule multi-prodotto (MPF)

Primi passi con le formule multi-prodotto (MPF)​

Versioni dei pacchetti

Il codice in questa pagina è stato sviluppato utilizzando i seguenti requisiti. Si consiglia di usare queste versioni o versioni più recenti.

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

Questa guida mostra come usare il pacchetto qiskit-addon-mpf, usando l'evoluzione temporale di un modello di Ising come esempio. Con questo pacchetto puoi costruire una Formula Multi-Prodotto (MPF) in grado di ottenere un errore di Trotter inferiore nelle misurazioni degli osservabili. Gli strumenti forniti ti permettono di determinare i pesi di un MPF scelto, che possono poi essere utilizzati per ricombinare i valori di aspettazione stimati da diversi circuiti di evoluzione temporale, ciascuno con un numero diverso di passi di Trotter.

Inizia considerando l'Hamiltoniano di un modello di Ising con 10 siti:

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

dove Ji,(i+1)J_{i,(i+1)} è la costante di accoppiamento e hih_i è l'intensità del campo magnetico esterno. Per impostare il problema, l'osservabile da misurare sarà la magnetizzazione totale del sistema

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

Il frammento di codice qui sotto prepara l'Hamiltoniano della catena di Ising usando il pacchetto qiskit-addon-utils e definisce l'osservabile che verrà misurato.

# 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])

In seguito prepari l'MPF. La prima scelta da fare è se i coefficienti saranno statici (indipendenti dal tempo) o dinamici; questo tutorial utilizza un MPF statico. La prossima scelta riguarda l'insieme dei valori kjk_j. Questo determina quanti termini ci saranno nell'MPF e quanti passi di Trotter ogni termine usa per simulare l'evoluzione temporale. La scelta dei valori kjk_j è euristica, quindi devi trovare un tuo insieme di valori kjk_j "buoni". Leggi le linee guida per trovare un buon insieme di valori alla fine della pagina di primi passi.

Successivamente, una volta determinati i valori kjk_j, puoi preparare il sistema di equazioni, Ax=bAx=b, da risolvere. La matrice AA è anch'essa determinata dalla formula di prodotto da usare. Le scelte qui riguardano il suo ordine, impostato a 22 in questo esempio, e se la formula di prodotto debba essere simmetrica, impostata a True per questo esempio. Il frammento di codice qui sotto seleziona un tempo totale per evolvere il sistema, i valori kjk_j da usare, e il sistema di equazioni da risolvere tramite il metodo 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 volta istanziato il sistema lineare di equazioni, questo può essere risolto esattamente oppure tramite un modello approssimato che utilizza una somma dei quadrati (o la norma di Frobenius per i coefficienti dinamici; consulta il riferimento API per ulteriori informazioni). La scelta di usare un modello approssimato sorge tipicamente quando la norma dei coefficienti per l'insieme di valori kjk_j scelto è ritenuta troppo alta e non è possibile scegliere un insieme di valori kjk_j diverso. Questa guida mostra entrambi i casi per confrontare i risultati.

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

L'oggetto LSE possiede anche un metodo LSE.solve(), che risolve il sistema di equazioni esattamente. Il motivo per cui in questa guida viene usato setup_exact_problem() è per illustrare l'interfaccia fornita dagli altri metodi approssimati.

Configurazione ed esecuzione dei circuiti di Trotter​

Ora che i coefficienti xjx_j sono stati ottenuti, l'ultimo passo consiste nel generare i circuiti di evoluzione temporale per l'ordine e l'insieme di passi kjk_j scelti dell'MPF. Il pacchetto qiskit-addon-utils può accelerare questo processo.

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)

Output of the previous code cell

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

Output of the previous code cell

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

Output of the previous code cell

Una volta costruiti questi circuiti, puoi quindi traspilarli ed eseguirli su un QPU. Per questo esempio, useremo semplicemente uno dei simulatori privi di rumore per dimostrare come viene ridotto l'errore di 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)]

Ricostruzione dei risultati​

Ora che i circuiti sono stati eseguiti, la ricostruzione dei risultati è abbastanza semplice. Come menzionato nella pagina di panoramica MPF, il nostro osservabile viene ricostruito tramite la somma pesata

⟨M⟩=∑jxj⟨Mj⟩.\langle M \rangle = \sum_j x_j \langle M_j \rangle.

dove xjx_j sono i coefficienti che abbiamo trovato e ⟨Mj⟩\langle M_j \rangle è la stima dell'osservabile ∑i⟨Zi⟩\sum_i \langle Z_i \rangle per ogni circuito. Possiamo poi confrontare i risultati ottenuti con il valore esatto usando il pacchetto 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

Qui puoi vedere che l'MPF ha ridotto l'errore di Trotter rispetto a quello ottenuto con una singola PF con kj=19k_j=19. Tuttavia, il modello approssimato ha prodotto un valore di aspettazione peggiore rispetto al modello esatto. Questo dimostra l'importanza di usare criteri di convergenza stringenti sul modello approssimato e di trovare un insieme "buono" di valori kjk_j.