Vai al contenuto principale

Compilazione quantistica approssimata per circuiti di evoluzione temporale

Stima di utilizzo: Cinque minuti su un processore Eagle (NOTA: Questa è solo una stima. Il tempo di esecuzione effettivo potrebbe variare.)

Contesto​

Questo tutorial dimostra come implementare la Compilazione Quantistica Approssimata utilizzando reti tensoriali (AQC-Tensor) con Qiskit per migliorare le prestazioni dei circuiti quantistici. Applichiamo AQC-Tensor nel contesto di un'evoluzione temporale trotterizzata per ridurre la profondità del circuito mantenendo l'accuratezza della simulazione, seguendo il framework Qiskit per la preparazione degli stati e l'ottimizzazione. Imparerete come creare un circuito ansatz a bassa profondità a partire da un circuito Trotter iniziale, ottimizzarlo con reti tensoriali e prepararlo per l'esecuzione su hardware quantistico.

L'obiettivo primario è simulare l'evoluzione temporale per un hamiltoniano modello con una profondità di circuito ridotta. Questo viene ottenuto utilizzando l'addon Qiskit AQC-Tensor, qiskit-addon-aqc-tensor, che sfrutta le reti tensoriali, in particolare gli stati a prodotto matriciale (MPS), per comprimere e ottimizzare il circuito iniziale. Attraverso regolazioni iterative, il circuito ansatz compresso mantiene la fedeltà al circuito originale rimanendo al contempo fattibile per l'hardware quantistico a breve termine. Maggiori dettagli possono essere trovati nella corrispondente documentazione con un esempio semplice per iniziare.

La Compilazione Quantistica Approssimata è particolarmente vantaggiosa nelle simulazioni quantistiche che superano i tempi di coerenza dell'hardware, poiché consente di eseguire simulazioni complesse in modo più efficiente. Questo tutorial vi guida attraverso la configurazione del flusso di lavoro AQC-Tensor in Qiskit, coprendo l'inizializzazione di un hamiltoniano, la generazione di circuiti Trotter e la traspilazione del circuito ottimizzato finale per un dispositivo target.

Requisiti​

Prima di iniziare questo tutorial, assicuratevi di avere installato quanto segue:

  • Qiskit SDK v1.0 o successivo, con supporto per la visualizzazione
  • Qiskit Runtime v0.22 o successivo (pip install qiskit-ibm-runtime)
  • Addon Qiskit AQC-Tensor (pip install 'qiskit-addon-aqc-tensor[aer,quimb-jax]')

Configurazione​

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-aqc-tensor qiskit-addon-utils qiskit-ibm-runtime quimb rustworkx scipy
import numpy as np
import quimb.tensor
import datetime
import matplotlib.pyplot as plt

from scipy.optimize import OptimizeResult, minimize

from qiskit.quantum_info import SparsePauliOp, Pauli
from qiskit.transpiler import CouplingMap
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit import QuantumCircuit
from qiskit.synthesis import SuzukiTrotter

from qiskit_addon_utils.problem_generators import (
generate_time_evolution_circuit,
)
from qiskit_addon_aqc_tensor.ansatz_generation import (
generate_ansatz_from_circuit,
)
from qiskit_addon_aqc_tensor.objective import MaximizeStateFidelity
from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator
from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit
from qiskit_addon_aqc_tensor.simulation import compute_overlap

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorV2 as Estimator

from rustworkx.visualization import graphviz_draw

Parte I. Esempio su piccola scala​

La prima parte di questo tutorial utilizza un esempio su piccola scala con 10 siti per illustrare il processo di mappatura di un problema di simulazione quantistica in un circuito quantistico eseguibile. Qui esploreremo le dinamiche di un modello XXZ a 10 siti, permettendoci di costruire e ottimizzare un circuito quantistico gestibile prima di passare a sistemi più grandi.

Il modello XXZ è ampiamente studiato in fisica per esaminare le interazioni di spin e le proprietà magnetiche. Impostiamo l'hamiltoniano in modo che abbia condizioni al contorno aperte con interazioni sito-dipendenti tra siti vicini lungo la catena.

Hamiltoniano modello e osservabile​

L'hamiltoniano per il nostro modello XXZ a 10 siti è definito come:

H^XXZ=∑i=1L−1Ji,(i+1)(XiX(i+1)+YiY(i+1)+2⋅ZiZ(i+1)) ,\hat{\mathcal{H}}_{XXZ} = \sum_{i=1}^{L-1} J_{i,(i+1)}\left(X_i X_{(i+1)}+Y_i Y_{(i+1)}+ 2\cdot Z_i Z_{(i+1)} \right) \, ,

dove Ji,(i+1)J_{i,(i+1)} è un coefficiente casuale corrispondente al legame (i,i+1)(i, i+1), e L=10L=10 è il numero di siti.

Simulando l'evoluzione di questo sistema con una profondità di circuito ridotta, possiamo ottenere informazioni sull'utilizzo di AQC-Tensor per comprimere e ottimizzare i circuiti.

Configurare l'hamiltoniano e l'osservabile​

Prima di mappare il nostro problema, dobbiamo configurare la mappa di accoppiamento, l'hamiltoniano e l'osservabile per il modello XXZ a 10 siti.

# L is the number of sites, also the length of the 1D spin chain
L = 10

# Generate the coupling map
edge_list = [(i - 1, i) for i in range(1, L)]
# Generate an edge-coloring so we can make hw-efficient circuits
even_edges = edge_list[::2]
odd_edges = edge_list[1::2]
coupling_map = CouplingMap(edge_list)

# Generate random coefficients for our XXZ Hamiltonian
np.random.seed(0)
Js = np.random.rand(L - 1) + 0.5 * np.ones(L - 1)
hamiltonian = SparsePauliOp(Pauli("I" * L))
for i, edge in enumerate(even_edges + odd_edges):
hamiltonian += SparsePauliOp.from_sparse_list(
[
("XX", (edge), Js[i] / 2),
("YY", (edge), Js[i] / 2),
("ZZ", (edge), Js[i]),
],
num_qubits=L,
)

# Generate a ZZ observable between the two middle qubits
observable = SparsePauliOp.from_sparse_list(
[("ZZ", (L // 2 - 1, L // 2), 1.0)], num_qubits=L
)

print("Hamiltonian:", hamiltonian)
print("Observable:", observable)
graphviz_draw(coupling_map.graph, method="circo")
Hamiltonian: SparsePauliOp(['IIIIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII', 'IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII'],
coeffs=[1. +0.j, 0.52440675+0.j, 0.52440675+0.j, 1.0488135 +0.j,
0.60759468+0.j, 0.60759468+0.j, 1.21518937+0.j, 0.55138169+0.j,
0.55138169+0.j, 1.10276338+0.j, 0.52244159+0.j, 0.52244159+0.j,
1.04488318+0.j, 0.4618274 +0.j, 0.4618274 +0.j, 0.9236548 +0.j,
0.57294706+0.j, 0.57294706+0.j, 1.14589411+0.j, 0.46879361+0.j,
0.46879361+0.j, 0.93758721+0.j, 0.6958865 +0.j, 0.6958865 +0.j,
1.391773 +0.j, 0.73183138+0.j, 0.73183138+0.j, 1.46366276+0.j])
Observable: SparsePauliOp(['IIIIZZIIII'],
coeffs=[1.+0.j])

Output of the previous code cell

Con l'hamiltoniano definito, possiamo procedere alla costruzione dello stato iniziale.

# Generate an initial state
initial_state = QuantumCircuit(L)
for i in range(L):
if i % 2:
initial_state.x(i)

Passo 1: Mappare gli input classici a un problema quantistico​

Ora che abbiamo costruito l'hamiltoniano, definendo le interazioni spin-spin e i campi magnetici esterni che caratterizzano il sistema, seguiamo tre passi principali nel flusso di lavoro AQC-Tensor:

  1. Generare il circuito AQC ottimizzato: Utilizzando la trotterizzazione, approssimiamo l'evoluzione iniziale, che viene poi compressa per ridurre la profondità del circuito.
  2. Creare il circuito di evoluzione temporale rimanente: Catturare l'evoluzione per il tempo rimanente oltre il segmento iniziale.
  3. Combinare i circuiti: Unire il circuito AQC ottimizzato con il circuito di evoluzione rimanente in un circuito di evoluzione temporale completo pronto per l'esecuzione.

Questo approccio crea un ansatz a bassa profondità per l'evoluzione target, supportando una simulazione efficiente entro i vincoli dell'hardware quantistico a breve termine.

Determinare la porzione di evoluzione temporale da simulare classicamente​

Il nostro obiettivo è simulare l'evoluzione temporale dell'hamiltoniano modello definito in precedenza utilizzando l'evoluzione Trotter. Per rendere questo processo efficiente per l'hardware quantistico, dividiamo l'evoluzione in due segmenti:

  • Segmento Iniziale: Questa porzione iniziale dell'evoluzione, da ti=0.0t_i = 0.0 a tf=0.2t_f = 0.2, è simulabile con MPS e può essere efficientemente "compilata" utilizzando AQC-Tensor. Utilizzando l'addon Qiskit AQC-Tensor, generiamo un circuito compresso per questo segmento, chiamato aqc_target_circuit. Poiché questo segmento sarà simulato su un simulatore tensor-network, possiamo permetterci di utilizzare un numero maggiore di strati Trotter senza impattare significativamente le risorse hardware. Impostiamo aqc_target_num_trotter_steps = 32 per questo segmento.

  • Segmento Successivo: Questa porzione rimanente dell'evoluzione, da t=0.2t = 0.2 a t=0.4t = 0.4, sarà eseguita su hardware quantistico, chiamata subsequent_circuit. Date le limitazioni hardware, miriamo a utilizzare il minor numero possibile di strati Trotter per mantenere una profondità di circuito gestibile. Per questo segmento, utilizziamo subsequent_num_trotter_steps = 3.

Scegliere il tempo di divisione​

Scegliamo t=0.2t = 0.2 come tempo di divisione per bilanciare la simulabilità classica con la fattibilità hardware. All'inizio dell'evoluzione, l'entanglement nel modello XXZ rimane sufficientemente basso affinché metodi classici come MPS possano approssimare accuratamente.

Quando si sceglie un tempo di divisione, una buona linea guida è selezionare un punto in cui l'entanglement è ancora gestibile classicamente ma cattura abbastanza dell'evoluzione per semplificare la porzione eseguita su hardware. Potrebbero essere necessari tentativi ed errori per trovare il miglior equilibrio per diversi hamiltoniani.

# Generate the AQC target circuit (initial segment)
aqc_evolution_time = 0.2
aqc_target_num_trotter_steps = 32

aqc_target_circuit = initial_state.copy()
aqc_target_circuit.compose(
generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),
time=aqc_evolution_time,
),
inplace=True,
)
# Generate the subsequent circuit
subsequent_num_trotter_steps = 3
subsequent_evolution_time = 0.2

subsequent_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=subsequent_num_trotter_steps),
time=subsequent_evolution_time,
)
subsequent_circuit.draw("mpl", fold=-1)

Output of the previous code cell

Per consentire un confronto significativo, genereremo due circuiti aggiuntivi:

  • Circuito di confronto AQC: Questo circuito evolve fino a aqc_evolution_time ma utilizza la stessa durata del passo Trotter del subsequent_circuit. Serve come confronto con il aqc_target_circuit, mostrando l'evoluzione che osserveremmo senza utilizzare un numero maggiore di passi Trotter. Ci riferiremo a questo circuito come aqc_comparison_circuit.

  • Circuito di riferimento: Questo circuito viene utilizzato come baseline per ottenere il risultato esatto. Simula l'evoluzione completa utilizzando reti tensoriali per calcolare il risultato esatto, fornendo un riferimento per valutare l'efficacia di AQC-Tensor. Ci riferiremo a questo circuito come reference_circuit.

# Generate the AQC comparison circuit
aqc_comparison_num_trotter_steps = int(
subsequent_num_trotter_steps
/ subsequent_evolution_time
* aqc_evolution_time
)
print(
"Number of Trotter steps for comparison:",
aqc_comparison_num_trotter_steps,
)

aqc_comparison_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_comparison_num_trotter_steps),
time=aqc_evolution_time,
)
Number of Trotter steps for comparison: 3
# Generate the reference circuit
evolution_time = 0.4
reps = 200

reference_circuit = initial_state.copy()
reference_circuit.compose(
generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=reps),
time=evolution_time,
),
inplace=True,
)

Generare un ansatz e parametri iniziali da un circuito Trotter con meno passi​

Ora che abbiamo costruito i nostri quattro circuiti, procediamo con il flusso di lavoro AQC-Tensor. Prima, costruiamo un circuito "buono" che ha lo stesso tempo di evoluzione del circuito target, ma con meno passi Trotter (e quindi meno strati).

Poi passiamo questo circuito "buono" alla funzione generate_ansatz_from_circuit di AQC-Tensor. Questa funzione analizza la connettività a due qubit del circuito e restituisce due cose:

  1. Un ansatz generale e parametrizzato con la stessa connettività a due qubit del circuito di input.
  2. Parametri che, quando inseriti nell'ansatz, producono il circuito (buono) di input.

Presto prenderemo questi parametri e li aggiusteremo iterativamente per portare il circuito ansatz il più vicino possibile all'MPS target.

aqc_ansatz_num_trotter_steps = 1

aqc_good_circuit = initial_state.copy()
aqc_good_circuit.compose(
generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),
time=aqc_evolution_time,
),
inplace=True,
)

aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(
aqc_good_circuit
)
aqc_ansatz.draw("mpl", fold=-1)

Output of the previous code cell

print(f"AQC Comparison circuit: depth {aqc_comparison_circuit.depth()}")
print(f"Target circuit: depth {aqc_target_circuit.depth()}")
print(
f"Ansatz circuit: depth {aqc_ansatz.depth()}, with {len(aqc_initial_parameters)} parameters"
)
AQC Comparison circuit: depth 36
Target circuit: depth 385
Ansatz circuit: depth 7, with 156 parameters

Scegliere le impostazioni per la simulazione della rete tensoriale​

Qui utilizziamo il simulatore di circuiti a stato prodotto matriciale di Quimb, insieme a jax per fornire il gradiente.

simulator_settings = QuimbSimulator(
quimb.tensor.CircuitMPS, autodiff_backend="jax"
)

Successivamente, costruiamo una rappresentazione MPS dello stato target che sarà approssimato utilizzando AQC-Tensor. Questa rappresentazione consente una gestione efficiente dell'entanglement, fornendo una descrizione compatta dello stato quantistico per un'ulteriore ottimizzazione.

aqc_target_mps = tensornetwork_from_circuit(
aqc_target_circuit, simulator_settings
)
print("Target MPS maximum bond dimension:", aqc_target_mps.psi.max_bond())

# Obtains the reference MPS, where we can obtain the exact expectation value by examining the `local_expectation``
reference_mps = tensornetwork_from_circuit(
reference_circuit, simulator_settings
)
reference_expval = reference_mps.local_expectation(
quimb.pauli("Z") & quimb.pauli("Z"), (L // 2 - 1, L // 2)
).real.item()
print("Reference MPS maximum bond dimension:", reference_mps.psi.max_bond())
Target MPS maximum bond dimension: 5
Reference MPS maximum bond dimension: 7

Notate che, scegliendo un numero maggiore di passi Trotter per lo stato target, abbiamo effettivamente ridotto il suo errore Trotter rispetto al circuito iniziale. Possiamo valutare la fedeltà (∣⟨ψ1∣ψ2⟩∣2|\langle \psi_1 | \psi_2 \rangle|^2) tra lo stato preparato dal circuito iniziale e lo stato target per quantificare questa differenza.

good_mps = tensornetwork_from_circuit(aqc_good_circuit, simulator_settings)
starting_fidelity = abs(compute_overlap(good_mps, aqc_target_mps)) ** 2
print("Starting fidelity:", starting_fidelity)
Starting fidelity: 0.9982464959067222

Ottimizzare i parametri dell'ansatz utilizzando calcoli MPS​

In questo passo, ottimizziamo i parametri dell'ansatz minimizzando una semplice funzione di costo, MaximizeStateFidelity, utilizzando l'ottimizzatore L-BFGS di SciPy. Selezioniamo un criterio di arresto per la fedeltà che garantisca il superamento della fedeltà del circuito iniziale senza AQC-Tensor. Una volta raggiunta questa soglia, il circuito compresso mostrerà sia un errore Trotter inferiore che una profondità ridotta rispetto al circuito originale. Utilizzando tempo CPU aggiuntivo, un'ulteriore ottimizzazione può continuare ad aumentare la fedeltà.

# Setting values for the optimization
aqc_stopping_fidelity = 1
aqc_max_iterations = 500

stopping_point = 1.0 - aqc_stopping_fidelity
objective = MaximizeStateFidelity(
aqc_target_mps, aqc_ansatz, simulator_settings
)

def callback(intermediate_result: OptimizeResult):
fidelity = 1 - intermediate_result.fun
print(
f"{datetime.datetime.now()} Intermediate result: Fidelity {fidelity:.8f}"
)
if intermediate_result.fun < stopping_point:
# Good enough for now
raise StopIteration

result = minimize(
objective,
aqc_initial_parameters,
method="L-BFGS-B",
jac=True,
options={"maxiter": aqc_max_iterations},
callback=callback,
)
if (
result.status
not in (
0,
1,
99,
)
): # 0 => success; 1 => max iterations reached; 99 => early termination via StopIteration
raise RuntimeError(
f"Optimization failed: {result.message} (status={result.status})"
)

print(f"Done after {result.nit} iterations.")
aqc_final_parameters = result.x
2025-04-14 11:46:52.174235 Intermediate result: Fidelity 0.99795851
2025-04-14 11:46:52.218249 Intermediate result: Fidelity 0.99822826
2025-04-14 11:46:52.280924 Intermediate result: Fidelity 0.99829675
2025-04-14 11:46:52.356214 Intermediate result: Fidelity 0.99832474
2025-04-14 11:46:52.411609 Intermediate result: Fidelity 0.99836131
2025-04-14 11:46:52.453747 Intermediate result: Fidelity 0.99839954
2025-04-14 11:46:52.496184 Intermediate result: Fidelity 0.99846517
2025-04-14 11:46:52.542046 Intermediate result: Fidelity 0.99865029
2025-04-14 11:46:52.583679 Intermediate result: Fidelity 0.99872332
2025-04-14 11:46:52.628732 Intermediate result: Fidelity 0.99892359
2025-04-14 11:46:52.690386 Intermediate result: Fidelity 0.99900640
2025-04-14 11:46:52.759398 Intermediate result: Fidelity 0.99907169
2025-04-14 11:46:52.819496 Intermediate result: Fidelity 0.99911423
2025-04-14 11:46:52.884505 Intermediate result: Fidelity 0.99918716
2025-04-14 11:46:52.947919 Intermediate result: Fidelity 0.99921278
2025-04-14 11:46:53.012808 Intermediate result: Fidelity 0.99924853
2025-04-14 11:46:53.083626 Intermediate result: Fidelity 0.99928797
2025-04-14 11:46:53.153235 Intermediate result: Fidelity 0.99933028
2025-04-14 11:46:53.221371 Intermediate result: Fidelity 0.99935757
2025-04-14 11:46:53.286211 Intermediate result: Fidelity 0.99938140
2025-04-14 11:46:53.352391 Intermediate result: Fidelity 0.99940964
2025-04-14 11:46:53.420472 Intermediate result: Fidelity 0.99944051
2025-04-14 11:46:53.486279 Intermediate result: Fidelity 0.99946828
2025-04-14 11:46:53.552338 Intermediate result: Fidelity 0.99948723
2025-04-14 11:46:53.618688 Intermediate result: Fidelity 0.99951011
2025-04-14 11:46:53.690878 Intermediate result: Fidelity 0.99954718
2025-04-14 11:46:53.762725 Intermediate result: Fidelity 0.99956267
2025-04-14 11:46:53.829784 Intermediate result: Fidelity 0.99958949
2025-04-14 11:46:53.897477 Intermediate result: Fidelity 0.99960498
2025-04-14 11:46:53.954633 Intermediate result: Fidelity 0.99961308
2025-04-14 11:46:54.010125 Intermediate result: Fidelity 0.99962894
2025-04-14 11:46:54.064717 Intermediate result: Fidelity 0.99964121
2025-04-14 11:46:54.118892 Intermediate result: Fidelity 0.99964348
2025-04-14 11:46:54.183236 Intermediate result: Fidelity 0.99964860
2025-04-14 11:46:54.245521 Intermediate result: Fidelity 0.99965695
2025-04-14 11:46:54.305792 Intermediate result: Fidelity 0.99966398
2025-04-14 11:46:54.355819 Intermediate result: Fidelity 0.99967816
2025-04-14 11:46:54.409580 Intermediate result: Fidelity 0.99968293
2025-04-14 11:46:54.457979 Intermediate result: Fidelity 0.99968936
2025-04-14 11:46:54.505891 Intermediate result: Fidelity 0.99969223
2025-04-14 11:46:54.551084 Intermediate result: Fidelity 0.99970009
2025-04-14 11:46:54.601817 Intermediate result: Fidelity 0.99970724
2025-04-14 11:46:54.650097 Intermediate result: Fidelity 0.99970987
2025-04-14 11:46:54.714727 Intermediate result: Fidelity 0.99971237
2025-04-14 11:46:54.780052 Intermediate result: Fidelity 0.99971916
2025-04-14 11:46:54.871994 Intermediate result: Fidelity 0.99971940
2025-04-14 11:46:54.958244 Intermediate result: Fidelity 0.99972465
2025-04-14 11:46:55.011057 Intermediate result: Fidelity 0.99972763
2025-04-14 11:46:55.175339 Intermediate result: Fidelity 0.99972894
2025-04-14 11:46:56.688912 Intermediate result: Fidelity 0.99972894
Done after 50 iterations.
parameters = [float(param) for param in aqc_final_parameters]
print("Final parameters:", parameters)
Final parameters: [-7.853983035039254, 1.5707966468427772, 1.5707962768868613, -1.570798010835122, 1.570794480409574, 1.5707972214146968, -1.570796593027083, 1.5707968206822998, -1.5707959018046258, -1.5707991700969144, 1.5707965852600927, 4.712386891737442, -7.853980840717957, 1.5707967508132654, 1.5707943162503217, -1.5707955382023582, 1.5707958007156742, 1.570796096113293, -1.5707928509846847, 1.5707971042943747, -1.570797909276557, -1.5707941020637393, 1.5707980179540793, 4.712389823219363, -1.5707928752386107, 1.5707996426312891, -1.5707975640471001, -1.570794132802984, 1.5707944361599957, 4.712390747060803, 0.1048818190315936, 0.06686710468840577, -0.0668645844756557, -3.1415923537135466, 1.2374931269696063, 6.323169390432535e-07, 3.53229204771738e-08, 2.1091105688681484, 6.283186439944202, 0.12152258846156239, 0.07961752617254866, -0.07961775088604585, -1.6564278051174865e-06, 2.0771163596472384, 3.141592651630471, -6.283185775192653, 1.7691609006726954, 3.1415922910116216, 0.19837572065074083, 0.11114901449078964, -0.11115124544944892, -3.141591983034976, 0.8570788408766729, 4.201601390404146e-07, -3.141593736550978, 0.34652010942396333, 6.283186232785291, 0.13606356527241956, 0.03891676349289617, -0.03891524189533726, -1.5707965732853424, 1.5707968967088564, -0.3086133992238162, 1.5707957152428194, 1.5707968398959653, -0.32062737993080026, 0.11027416939993417, 0.0726167290795046, -0.07262020423334464, -2.3729431959735024e-06, 1.8204437429254703, 9.299060301196612e-07, -3.141592899563451, 2.103269568939461, 3.1415937539734626, 0.11536891854817125, 0.09099022308254198, -0.09098864958606581, -3.1415913307373127, 2.078429034357281, -1.509777998069368e-06, -3.1415922600663255, 1.5189162645358172, -3.1415878461323583, 0.09999070991480716, 0.04352011445148391, -0.04351849541849812, -1.570797642506462, 1.570795238023824, 0.8903442644396505, 1.5707962698006606, 1.5707946765132268, 0.9098791754570567, 0.10448284343424026, 0.07317037684936827, -0.07316718173961152, -3.141592682240966, 2.1665363080039612, -7.450882112394189e-07, -5.771181304929921e-07, 2.615334999517103, -3.1415914971653898, 0.1890887078648001, 0.13578163074571992, -0.13578078143610256, 7.156734195912883e-07, 1.7915385305413096, -5.188866034727312e-07, 1.2827742939197711e-06, 1.2348316581417487, 6.28318357406372, 0.08061187643781703, 0.03820789039271876, -0.03820731868804904, 1.5707964027727628, 1.570798734462218, 4.387336153720882, -1.570795722044763, 1.570798457375325, 4.450361734163248, 0.092360147257953, 0.06047700345049011, -0.06048592856713045, -3.141591214829027, 2.6593289993286047, -2.366937342261038e-07, 8.112162974032695e-08, 1.8907014631413432, 8.355881261853104e-07, 0.23303641819370874, 0.14331998953606456, -0.1433194488304741, -3.141591621822901, 0.7455776479558791, 3.1415914520163586, -3.1415933560496105, 0.7603938554148255, -1.6230983177616282e-06, 0.07186349688535713, 0.03197144517771341, -0.031971177878588546, -4.712389048748508, 1.5707948403165752, 1.2773619319829186, -1.5707990802172127, 1.5707957676951863, 1.289083769394045, 0.13644999397718796, 0.032761460443590046, -0.032762060585195645, -1.5707977610073176, 1.5707964181578042, -3.4826435600366983, -4.712389691708343, 1.570794277502252, 2.799088046133275]

A questo punto, è necessario trovare solo i parametri finali per il circuito ansatz. Possiamo quindi unire il circuito AQC ottimizzato con il circuito di evoluzione rimanente per creare un circuito di evoluzione temporale completo per l'esecuzione su hardware quantistico.

aqc_final_circuit = aqc_ansatz.assign_parameters(aqc_final_parameters)
aqc_final_circuit.compose(subsequent_circuit, inplace=True)
aqc_final_circuit.draw("mpl", fold=-1)

Output of the previous code cell

Dobbiamo anche unire il nostro aqc_comparison_circuit con il circuito di evoluzione rimanente. Questo circuito sarà utilizzato per confrontare le prestazioni del circuito ottimizzato con AQC-Tensor con il circuito originale.

aqc_comparison_circuit.compose(subsequent_circuit, inplace=True)
aqc_comparison_circuit.draw("mpl", fold=-1)

Output of the previous code cell

Step 2: Ottimizzare il problema per l'esecuzione su hardware quantistico​

Selezionate l'hardware. Qui utilizzeremo uno qualsiasi dei dispositivi IBM Quantum® disponibili che abbiano almeno 127 qubit.

service = QiskitRuntimeService()
backend = service.least_busy(min_num_qubits=127)
print(backend)

Traspiliamo i PUB (circuiti e osservabili) per adattarli all'ISA (Instruction Set Architecture) del backend. Impostando optimization_level=3, il transpiler ottimizza il circuito per adattarlo a una catena unidimensionale di qubit, riducendo il rumore che impatta sulla fedeltà del circuito. Una volta che i circuiti sono trasformati in un formato compatibile con il backend, applichiamo una trasformazione corrispondente agli osservabili per assicurare che si allineino con il layout di qubit modificato.

pass_manager = generate_preset_pass_manager(
backend=backend, optimization_level=3
)
isa_circuit = pass_manager.run(aqc_final_circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
print("Observable info:", isa_observable)
print("Circuit depth:", isa_circuit.depth())
isa_circuit.draw("mpl", fold=-1, idle_wires=False)
Observable info: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ'],
coeffs=[1.+0.j])
Circuit depth: 111

Output of the previous code cell

Eseguite la transpilazione per il circuito di confronto.

isa_comparison_circuit = pass_manager.run(aqc_comparison_circuit)
isa_comparison_observable = observable.apply_layout(
isa_comparison_circuit.layout
)
print("Observable info:", isa_comparison_observable)
print("Circuit depth:", isa_comparison_circuit.depth())
isa_comparison_circuit.draw("mpl", fold=-1, idle_wires=False)
Observable info: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ'],
coeffs=[1.+0.j])
Circuit depth: 158

Output of the previous code cell

Step 3: Eseguire utilizzando le primitive Qiskit​

In questo passaggio, eseguiamo il circuito transpilato su hardware quantistico (o un backend simulato). Utilizzando la classe EstimatorV2 da qiskit_ibm_runtime, configuriamo un estimator per eseguire il circuito e misurare l'osservabile specificato. Il risultato del job fornisce l'outcome atteso per l'osservabile, dandoci informazioni sulle prestazioni del circuito sull'hardware di destinazione.

estimator = Estimator(backend)
job = estimator.run([(isa_circuit, isa_observable)])
print("Job ID:", job.job_id())
job.result()
Job ID: czyhqdxd8drg008hx0yg
PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(<shape=(), dtype=float64>), stds=np.ndarray(<shape=(), dtype=float64>), ensemble_standard_error=np.ndarray(<shape=(), dtype=float64>)), metadata={'shots': 4096, 'target_precision': 0.015625, 'circuit_metadata': {}, 'resilience': {}, 'num_randomizations': 32})], metadata={'dynamical_decoupling': {'enable': False, 'sequence_type': 'XX', 'extra_slack_distribution': 'middle', 'scheduling_method': 'alap'}, 'twirling': {'enable_gates': False, 'enable_measure': True, 'num_randomizations': 'auto', 'shots_per_randomization': 'auto', 'interleave_randomizations': True, 'strategy': 'active-accum'}, 'resilience': {'measure_mitigation': True, 'zne_mitigation': False, 'pec_mitigation': False}, 'version': 2})

Eseguite l'esecuzione per il circuito di confronto.

job_comparison = estimator.run([(isa_comparison_circuit, isa_observable)])
print("Job Comparison ID:", job.job_id())
job_comparison.result()
Job Comparison ID: czyhqdxd8drg008hx0yg
PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(<shape=(), dtype=float64>), stds=np.ndarray(<shape=(), dtype=float64>), ensemble_standard_error=np.ndarray(<shape=(), dtype=float64>)), metadata={'shots': 4096, 'target_precision': 0.015625, 'circuit_metadata': {}, 'resilience': {}, 'num_randomizations': 32})], metadata={'dynamical_decoupling': {'enable': False, 'sequence_type': 'XX', 'extra_slack_distribution': 'middle', 'scheduling_method': 'alap'}, 'twirling': {'enable_gates': False, 'enable_measure': True, 'num_randomizations': 'auto', 'shots_per_randomization': 'auto', 'interleave_randomizations': True, 'strategy': 'active-accum'}, 'resilience': {'measure_mitigation': True, 'zne_mitigation': False, 'pec_mitigation': False}, 'version': 2})

Step 4: Post-elaborare e restituire il risultato nel formato classico desiderato​

In questo caso, la ricostruzione non è necessaria. Possiamo esaminare direttamente il risultato accedendo al valore di aspettazione dall'output dell'esecuzione.

# AQC results
hw_results = job.result()
hw_results_dicts = [pub_result.data.__dict__ for pub_result in hw_results]
hw_expvals = [
pub_result_data["evs"].tolist() for pub_result_data in hw_results_dicts
]
aqc_expval = hw_expvals[0]

# AQC comparison results
hw_comparison_results = job_comparison.result()
hw_comparison_results_dicts = [
pub_result.data.__dict__ for pub_result in hw_comparison_results
]
hw_comparison_expvals = [
pub_result_data["evs"].tolist()
for pub_result_data in hw_comparison_results_dicts
]
aqc_compare_expval = hw_comparison_expvals[0]

print(f"Exact: \t{reference_expval:.4f}")
print(
f"AQC: \t{aqc_expval:.4f}, |∆| = {np.abs(reference_expval- aqc_expval):.4f}"
)
print(
f"AQC Comparison:\t{aqc_compare_expval:.4f}, |∆| = {np.abs(reference_expval- aqc_compare_expval):.4f}"
)
Exact:         	-0.5252
AQC: -0.4903, |∆| = 0.0349
AQC Comparison: 0.5424, |∆| = 1.0676

Grafico a barre per confrontare i risultati dei circuiti AQC, di confronto ed esatto.

plt.style.use("seaborn-v0_8")

labels = ["AQC Result", "AQC Comparison Result"]
values = [abs(aqc_expval), abs(aqc_compare_expval)]

plt.figure(figsize=(10, 6))
bars = plt.bar(labels, values, color=["tab:blue", "tab:purple"])
plt.axhline(
y=abs(reference_expval), color="red", linestyle="--", label="Exact Result"
)
plt.xlabel("Results")
plt.ylabel("Absolute Expected Value")
plt.title("AQC Result vs AQC Comparison Result (Absolute Values)")
plt.legend()
for bar in bars:
y_val = bar.get_height()
plt.text(
bar.get_x() + bar.get_width() / 2.0,
y_val,
round(y_val, 2),
va="bottom",
)
plt.show()

Output of the previous code cell

Parte II: aumentare la scala​

La seconda parte di questo tutorial si basa sull'esempio precedente aumentando la scala fino a un sistema più grande con 50 siti, illustrando come mappare problemi di simulazione quantistica più complessi a circuiti quantistici eseguibili. Qui esploriamo la dinamica di un modello XXZ con 50 siti, permettendoci di costruire e ottimizzare un circuito quantistico sostanziale che riflette dimensioni di sistema più realistiche.

L'Hamiltoniana per il nostro modello XXZ a 50 siti è definita come:

H^XXZ=∑i=1L−1Ji,(i+1)(XiX(i+1)+YiY(i+1)+2⋅ZiZ(i+1)) ,\hat{\mathcal{H}}_{XXZ} = \sum_{i=1}^{L-1} J_{i,(i+1)}\left(X_i X_{(i+1)}+Y_i Y_{(i+1)}+ 2\cdot Z_i Z_{(i+1)} \right) \, ,

dove Ji,(i+1)J_{i,(i+1)} è un coefficiente casuale corrispondente all'arco (i,i+1)(i, i+1), e L=50L=50 è il numero di siti. Definite la mappa di accoppiamento e gli archi per l'Hamiltoniana.

L = 50  # L = length of our 1D spin chain

# Generate the edge list for this spin-chain
edge_list = [(i - 1, i) for i in range(1, L)]
# Generate an edge-coloring so we can make hw-efficient circuits
even_edges = edge_list[::2]
odd_edges = edge_list[1::2]

# Instantiate a CouplingMap object
coupling_map = CouplingMap(edge_list)

# Generate random coefficients for our XXZ Hamiltonian
np.random.seed(0)
Js = np.random.rand(L - 1) + 0.5 * np.ones(L - 1)

hamiltonian = SparsePauliOp(Pauli("I" * L))
for i, edge in enumerate(even_edges + odd_edges):
hamiltonian += SparsePauliOp.from_sparse_list(
[
("XX", (edge), Js[i] / 2),
("YY", (edge), Js[i] / 2),
("ZZ", (edge), Js[i]),
],
num_qubits=L,
)

observable = SparsePauliOp.from_sparse_list(
[("ZZ", (L // 2 - 1, L // 2), 1.0)], num_qubits=L
)

# Generate an initial state
L = hamiltonian.num_qubits
initial_state = QuantumCircuit(L)
for i in range(L):
if i % 2:
initial_state.x(i)

Step 1: Mappare gli input classici a un problema quantistico​

Per questo problema più grande, iniziamo costruendo l'Hamiltoniana per il modello XXZ a 50 siti, definendo le interazioni spin-spin e i campi magnetici esterni su tutti i siti. Dopo questo, seguiamo tre passaggi principali:

  1. Generare il circuito AQC ottimizzato: Utilizzare la trotterizzazione per approssimare l'evoluzione iniziale, quindi comprimere questo segmento per ridurre la profondità del circuito.
  2. Creare il circuito di evoluzione temporale rimanente: Catturare l'evoluzione temporale rimanente oltre il segmento iniziale.
  3. Combinare i circuiti: Unire il circuito AQC ottimizzato con il circuito di evoluzione rimanente per creare un circuito di evoluzione temporale completo pronto per l'esecuzione. Generare il circuito target AQC (il segmento iniziale).
aqc_evolution_time = 0.2
aqc_target_num_trotter_steps = 32

aqc_target_circuit = initial_state.copy()
aqc_target_circuit.compose(
generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),
time=aqc_evolution_time,
),
inplace=True,
)

Generare il circuito successivo (il segmento rimanente).

subsequent_num_trotter_steps = 3
subsequent_evolution_time = 0.2

subsequent_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=subsequent_num_trotter_steps),
time=subsequent_evolution_time,
)

Generare il circuito di confronto AQC (il segmento iniziale, ma con lo stesso numero di passaggi di Trotter del circuito successivo).

# Generate the AQC comparison circuit
aqc_comparison_num_trotter_steps = int(
subsequent_num_trotter_steps
/ subsequent_evolution_time
* aqc_evolution_time
)
print(
"Number of Trotter steps for comparison:",
aqc_comparison_num_trotter_steps,
)

aqc_comparison_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_comparison_num_trotter_steps),
time=aqc_evolution_time,
)
Number of Trotter steps for comparison: 3

Generare il circuito di riferimento.

evolution_time = 0.4
reps = 200

reference_circuit = initial_state.copy()
reference_circuit.compose(
generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=reps),
time=evolution_time,
),
inplace=True,
)

Generare un ansatz e i parametri iniziali da un circuito di Trotter con meno passaggi.

aqc_ansatz_num_trotter_steps = 1

aqc_good_circuit = initial_state.copy()
aqc_good_circuit.compose(
generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),
time=aqc_evolution_time,
),
inplace=True,
)

aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(
aqc_good_circuit
)
print(f"AQC Comparison circuit: depth {aqc_comparison_circuit.depth()}")
print(f"Target circuit: depth {aqc_target_circuit.depth()}")
print(
f"Ansatz circuit: depth {aqc_ansatz.depth()}, with {len(aqc_initial_parameters)} parameters"
)
AQC Comparison circuit: depth 36
Target circuit: depth 385
Ansatz circuit: depth 7, with 816 parameters

Impostate le configurazioni per la simulazione a rete tensoriale e quindi costruite una rappresentazione di stato del prodotto a matrici dello stato target per l'ottimizzazione. Poi, valutate la fedeltà tra il circuito iniziale e lo stato target per quantificare la differenza nell'errore di Trotter.

simulator_settings = QuimbSimulator(
quimb.tensor.CircuitMPS, autodiff_backend="jax"
)

# Build the matrix-product representation of the state to be approximated by AQC
aqc_target_mps = tensornetwork_from_circuit(
aqc_target_circuit, simulator_settings
)
print("Target MPS maximum bond dimension:", aqc_target_mps.psi.max_bond())

# Obtains the reference MPS, where we can obtain the exact expectation value by examining the `local_expectation``
reference_mps = tensornetwork_from_circuit(
reference_circuit, simulator_settings
)
reference_expval = reference_mps.local_expectation(
quimb.pauli("Z") & quimb.pauli("Z"), (L // 2 - 1, L // 2)
).real.item()

# Compute the starting fidelity
good_mps = tensornetwork_from_circuit(aqc_good_circuit, simulator_settings)
starting_fidelity = abs(compute_overlap(good_mps, aqc_target_mps)) ** 2
print("Starting fidelity:", starting_fidelity)
Target MPS maximum bond dimension: 5
Starting fidelity: 0.9926466919924161

Per ottimizzare i parametri dell'ansatz, minimizziamo la funzione di costo MaximizeStateFidelity utilizzando l'ottimizzatore L-BFGS da SciPy, con un criterio di arresto impostato per superare la fedeltà del circuito iniziale senza AQC-Tensor. Questo assicura che il circuito compresso abbia sia un errore di Trotter inferiore che una profondità ridotta.

# Setting values for the optimization
aqc_stopping_fidelity = 1
aqc_max_iterations = 500

stopping_point = 1.0 - aqc_stopping_fidelity
objective = MaximizeStateFidelity(
aqc_target_mps, aqc_ansatz, simulator_settings
)

def callback(intermediate_result: OptimizeResult):
fidelity = 1 - intermediate_result.fun
print(
f"{datetime.datetime.now()} Intermediate result: Fidelity {fidelity:.8f}"
)
if intermediate_result.fun < stopping_point:
# Good enough for now
raise StopIteration

result = minimize(
objective,
aqc_initial_parameters,
method="L-BFGS-B",
jac=True,
options={"maxiter": aqc_max_iterations},
callback=callback,
)
if (
result.status
not in (
0,
1,
99,
)
): # 0 => success; 1 => max iterations reached; 99 => early termination via StopIteration
raise RuntimeError(
f"Optimization failed: {result.message} (status={result.status})"
)

print(f"Done after {result.nit} iterations.")
aqc_final_parameters = result.x
2025-04-14 11:48:28.705807 Intermediate result: Fidelity 0.99795851
2025-04-14 11:48:28.743265 Intermediate result: Fidelity 0.99822826
2025-04-14 11:48:28.776629 Intermediate result: Fidelity 0.99829675
2025-04-14 11:48:28.816153 Intermediate result: Fidelity 0.99832474
2025-04-14 11:48:28.856437 Intermediate result: Fidelity 0.99836131
2025-04-14 11:48:28.896432 Intermediate result: Fidelity 0.99839954
2025-04-14 11:48:28.936670 Intermediate result: Fidelity 0.99846517
2025-04-14 11:48:28.982069 Intermediate result: Fidelity 0.99865029
2025-04-14 11:48:29.026130 Intermediate result: Fidelity 0.99872332
2025-04-14 11:48:29.067426 Intermediate result: Fidelity 0.99892359
2025-04-14 11:48:29.110742 Intermediate result: Fidelity 0.99900640
2025-04-14 11:48:29.161362 Intermediate result: Fidelity 0.99907169
2025-04-14 11:48:29.207933 Intermediate result: Fidelity 0.99911423
2025-04-14 11:48:29.266772 Intermediate result: Fidelity 0.99918716
2025-04-14 11:48:29.331727 Intermediate result: Fidelity 0.99921278
2025-04-14 11:48:29.401694 Intermediate result: Fidelity 0.99924853
2025-04-14 11:48:29.467980 Intermediate result: Fidelity 0.99928797
2025-04-14 11:48:29.533281 Intermediate result: Fidelity 0.99933028
2025-04-14 11:48:29.600833 Intermediate result: Fidelity 0.99935757
2025-04-14 11:48:29.670816 Intermediate result: Fidelity 0.99938140
2025-04-14 11:48:29.736928 Intermediate result: Fidelity 0.99940964
2025-04-14 11:48:29.802931 Intermediate result: Fidelity 0.99944051
2025-04-14 11:48:29.869177 Intermediate result: Fidelity 0.99946828
2025-04-14 11:48:29.940156 Intermediate result: Fidelity 0.99948723
2025-04-14 11:48:30.005751 Intermediate result: Fidelity 0.99951011
2025-04-14 11:48:30.070853 Intermediate result: Fidelity 0.99954718
2025-04-14 11:48:30.139171 Intermediate result: Fidelity 0.99956267
2025-04-14 11:48:30.210506 Intermediate result: Fidelity 0.99958949
2025-04-14 11:48:30.279647 Intermediate result: Fidelity 0.99960498
2025-04-14 11:48:30.348016 Intermediate result: Fidelity 0.99961308
2025-04-14 11:48:30.414311 Intermediate result: Fidelity 0.99962894
2025-04-14 11:48:30.488910 Intermediate result: Fidelity 0.99964121
2025-04-14 11:48:30.561298 Intermediate result: Fidelity 0.99964348
2025-04-14 11:48:30.632214 Intermediate result: Fidelity 0.99964860
2025-04-14 11:48:30.705703 Intermediate result: Fidelity 0.99965695
2025-04-14 11:48:30.775679 Intermediate result: Fidelity 0.99966398
2025-04-14 11:48:30.842629 Intermediate result: Fidelity 0.99967816
2025-04-14 11:48:30.912357 Intermediate result: Fidelity 0.99968293
2025-04-14 11:48:30.979420 Intermediate result: Fidelity 0.99968936
2025-04-14 11:48:31.049196 Intermediate result: Fidelity 0.99969223
2025-04-14 11:48:31.125391 Intermediate result: Fidelity 0.99970009
2025-04-14 11:48:31.201256 Intermediate result: Fidelity 0.99970724
2025-04-14 11:48:31.272424 Intermediate result: Fidelity 0.99970987
2025-04-14 11:48:31.338907 Intermediate result: Fidelity 0.99971237
2025-04-14 11:48:31.404800 Intermediate result: Fidelity 0.99971916
2025-04-14 11:48:31.475226 Intermediate result: Fidelity 0.99971940
2025-04-14 11:48:31.547746 Intermediate result: Fidelity 0.99972465
2025-04-14 11:48:31.622827 Intermediate result: Fidelity 0.99972763
2025-04-14 11:48:31.819516 Intermediate result: Fidelity 0.99972894
2025-04-14 11:48:33.444538 Intermediate result: Fidelity 0.99972894
Done after 50 iterations.
parameters = [float(param) for param in aqc_final_parameters]

Costruite il circuito finale per la transpilazione assemblando l'ansatz ottimizzato con il circuito di evoluzione temporale rimanente.

aqc_final_circuit = aqc_ansatz.assign_parameters(aqc_final_parameters)
aqc_final_circuit.compose(subsequent_circuit, inplace=True)
aqc_comparison_circuit.compose(subsequent_circuit, inplace=True)

Passo 2: Ottimizzare il problema per l'esecuzione su hardware quantistico​

Selezionate il backend.

service = QiskitRuntimeService()
backend = service.least_busy(min_num_qubits=127)
print(backend)

Traspilateil circuito completato sull'hardware di destinazione, preparandolo per l'esecuzione. Il circuito ISA risultante può quindi essere inviato per l'esecuzione sul backend.

pass_manager = generate_preset_pass_manager(
backend=backend, optimization_level=3
)
isa_circuit = pass_manager.run(aqc_final_circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
print("Observable info:", isa_observable)
print("Circuit depth:", isa_circuit.depth())
isa_circuit.draw("mpl", fold=-1, idle_wires=False)
Observable info: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])
Circuit depth: 122

Output of the previous code cell

isa_comparison_circuit = pass_manager.run(aqc_comparison_circuit)
isa_comparison_observable = observable.apply_layout(
isa_comparison_circuit.layout
)
print("Observable info:", isa_comparison_observable)
print("Circuit depth:", isa_comparison_circuit.depth())
isa_comparison_circuit.draw("mpl", fold=-1, idle_wires=False)
Observable info: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])
Circuit depth: 158

Output of the previous code cell

Passo 3: Eseguire utilizzando le primitive Qiskit​

In questo passo, eseguiamo il circuito traspilato su hardware quantistico (o un backend simulato) utilizzando EstimatorV2 da qiskit_ibm_runtime per misurare l'osservabile specificata. Il risultato del job fornirà informazioni preziose sulle prestazioni del circuito sull'hardware di destinazione.

Per questo esempio su scala più ampia, esploreremo come utilizzare EstimatorOptions per gestire e controllare meglio i parametri del nostro esperimento hardware. Sebbene queste impostazioni siano opzionali, sono utili per tracciare i parametri dell'esperimento e perfezionare le opzioni di esecuzione per ottenere risultati ottimali.

Per un elenco completo delle opzioni di esecuzione disponibili, consultate la documentazione di qiskit-ibm-runtime.

twirling_options = {
"enable_gates": True,
"enable_measure": True,
"num_randomizations": 300,
"shots_per_randomization": 100,
"strategy": "active",
}

zne_options = {
"amplifier": "gate_folding",
"noise_factors": [1, 2, 3],
"extrapolated_noise_factors": list(np.linspace(0, 3, 31)),
"extrapolator": ["exponential", "linear", "fallback"],
}

meas_learning_options = {
"num_randomizations": 512,
"shots_per_randomization": 512,
}

resilience_options = {
"measure_mitigation": True,
"zne_mitigation": True,
"zne": zne_options,
"measure_noise_learning": meas_learning_options,
}

estimator_options = {
"resilience": resilience_options,
"twirling": twirling_options,
}

estimator = Estimator(backend, options=estimator_options)
job = estimator.run([(isa_circuit, isa_observable)])
print("Job ID:", job.job_id())
job.result()
Job ID: czyjx6crxz8g008f63r0
PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(<shape=(), dtype=float64>), stds=np.ndarray(<shape=(), dtype=float64>), evs_noise_factors=np.ndarray(<shape=(3,), dtype=float64>), stds_noise_factors=np.ndarray(<shape=(3,), dtype=float64>), ensemble_stds_noise_factors=np.ndarray(<shape=(3,), dtype=float64>), evs_extrapolated=np.ndarray(<shape=(3, 31), dtype=float64>), stds_extrapolated=np.ndarray(<shape=(3, 31), dtype=float64>)), metadata={'shots': 30000, 'target_precision': 0.005773502691896258, 'circuit_metadata': {}, 'resilience': {'zne': {'extrapolator': 'exponential'}}, 'num_randomizations': 300})], metadata={'dynamical_decoupling': {'enable': False, 'sequence_type': 'XX', 'extra_slack_distribution': 'middle', 'scheduling_method': 'alap'}, 'twirling': {'enable_gates': True, 'enable_measure': True, 'num_randomizations': 300, 'shots_per_randomization': 100, 'interleave_randomizations': True, 'strategy': 'active'}, 'resilience': {'measure_mitigation': True, 'zne_mitigation': True, 'pec_mitigation': False, 'zne': {'noise_factors': [1, 2, 3], 'extrapolator': ['exponential', 'linear', 'fallback'], 'extrapolated_noise_factors': [0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1, 1.1, 1.2000000000000002, 1.3, 1.4000000000000001, 1.5, 1.6, 1.7000000000000002, 1.8, 1.9000000000000001, 2, 2.1, 2.2, 2.3000000000000003, 2.4000000000000004, 2.5, 2.6, 2.7, 2.8000000000000003, 2.9000000000000004, 3]}}, 'version': 2})
job_comparison = estimator.run([(isa_comparison_circuit, isa_observable)])
print("Job Comparison ID:", job.job_id())
job_comparison.result()
Job Comparison ID: czyjx6crxz8g008f63r0
PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(<shape=(), dtype=float64>), stds=np.ndarray(<shape=(), dtype=float64>), evs_noise_factors=np.ndarray(<shape=(3,), dtype=float64>), stds_noise_factors=np.ndarray(<shape=(3,), dtype=float64>), ensemble_stds_noise_factors=np.ndarray(<shape=(3,), dtype=float64>), evs_extrapolated=np.ndarray(<shape=(3, 31), dtype=float64>), stds_extrapolated=np.ndarray(<shape=(3, 31), dtype=float64>)), metadata={'shots': 30000, 'target_precision': 0.005773502691896258, 'circuit_metadata': {}, 'resilience': {'zne': {'extrapolator': 'exponential'}}, 'num_randomizations': 300})], metadata={'dynamical_decoupling': {'enable': False, 'sequence_type': 'XX', 'extra_slack_distribution': 'middle', 'scheduling_method': 'alap'}, 'twirling': {'enable_gates': True, 'enable_measure': True, 'num_randomizations': 300, 'shots_per_randomization': 100, 'interleave_randomizations': True, 'strategy': 'active'}, 'resilience': {'measure_mitigation': True, 'zne_mitigation': True, 'pec_mitigation': False, 'zne': {'noise_factors': [1, 2, 3], 'extrapolator': ['exponential', 'linear', 'fallback'], 'extrapolated_noise_factors': [0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1, 1.1, 1.2000000000000002, 1.3, 1.4000000000000001, 1.5, 1.6, 1.7000000000000002, 1.8, 1.9000000000000001, 2, 2.1, 2.2, 2.3000000000000003, 2.4000000000000004, 2.5, 2.6, 2.7, 2.8000000000000003, 2.9000000000000004, 3]}}, 'version': 2})

Passo 4: Post-elaborare e restituire il risultato nel formato classico desiderato​

Qui, non è necessaria alcuna ricostruzione, come in precedenza; possiamo accedere direttamente al valore di aspettazione dall'output di esecuzione per esaminare il risultato.

# AQC results
hw_results = job.result()
hw_results_dicts = [pub_result.data.__dict__ for pub_result in hw_results]
hw_expvals = [
pub_result_data["evs"].tolist() for pub_result_data in hw_results_dicts
]
aqc_expval = hw_expvals[0]

# AQC comparison results
hw_comparison_results = job_comparison.result()
hw_comparison_results_dicts = [
pub_result.data.__dict__ for pub_result in hw_comparison_results
]
hw_comparison_expvals = [
pub_result_data["evs"].tolist()
for pub_result_data in hw_comparison_results_dicts
]
aqc_compare_expval = hw_comparison_expvals[0]

print(f"Exact: \t{reference_expval:.4f}")
print(
f"AQC: \t{aqc_expval:.4f}, |∆| = {np.abs(reference_expval- aqc_expval):.4f}"
)
print(
f"AQC Comparison:\t{aqc_compare_expval:.4f}, |∆| = {np.abs(reference_expval- aqc_compare_expval):.4f}"
)
Exact:         	-0.5888
AQC: -0.4809, |∆| = 0.1078
AQC Comparison: 1.1764, |∆| = 1.7652

Rappresentate graficamente i risultati dei circuiti AQC, di confronto ed esatto per il modello XXZ a 50 siti.

labels = ["AQC Result", "AQC Comparison Result"]
values = [abs(aqc_expval), abs(aqc_compare_expval)]

plt.figure(figsize=(10, 6))
bars = plt.bar(labels, values, color=["tab:blue", "tab:purple"])
plt.axhline(
y=abs(reference_expval), color="red", linestyle="--", label="Exact Result"
)
plt.xlabel("Results")
plt.ylabel("Absolute Expected Value")
plt.title("AQC Result vs AQC Comparison Result (Absolute Values)")
plt.legend()
for bar in bars:
y_val = bar.get_height()
plt.text(
bar.get_x() + bar.get_width() / 2.0,
y_val,
round(y_val, 2),
va="bottom",
)

plt.show()

Output of the previous code cell

Conclusione​

Questo tutorial ha dimostrato come utilizzare la Compilazione Quantistica Approssimata con reti tensoriali (AQC-Tensor) per comprimere e ottimizzare circuiti per simulare dinamiche quantistiche su scala. Utilizzando modelli di Heisenberg sia piccoli che grandi, abbiamo applicato AQC-Tensor per ridurre la profondità del circuito richiesta per l'evoluzione temporale trotterizzata. Generando un ansatz parametrizzato da un circuito di Trotter semplificato e ottimizzandolo con tecniche di stati a prodotto di matrici (MPS), abbiamo ottenuto un'approssimazione a bassa profondità dell'evoluzione target che è sia accurata che efficiente.

Il flusso di lavoro qui presentato evidenzia i principali vantaggi di AQC-Tensor per scalare le simulazioni quantistiche:

  • Compressione significativa del circuito: AQC-Tensor ha ridotto la profondità del circuito necessaria per l'evoluzione temporale complessa, migliorando la sua fattibilità sui dispositivi attuali.
  • Ottimizzazione efficiente: L'approccio MPS ha fornito un framework robusto per l'ottimizzazione dei parametri, bilanciando fedeltà ed efficienza computazionale.
  • Esecuzione pronta per l'hardware: La transpilazione del circuito ottimizzato finale ha assicurato che soddisfacesse i vincoli dell'hardware quantistico di destinazione.

Con l'emergere di dispositivi quantistici più grandi e algoritmi più avanzati, tecniche come AQC-Tensor diventeranno essenziali per eseguire simulazioni quantistiche complesse su hardware a breve termine, dimostrando progressi promettenti nella gestione della profondità e della fedeltà per applicazioni quantistiche scalabili.

Sondaggio sul tutorial​

Per favore, partecipate a questo breve sondaggio per fornire un feedback su questo tutorial. Le vostre opinioni ci aiuteranno a migliorare i nostri contenuti e l'esperienza utente.

Link to survey