Ridurre la profondità del circuito con il Qiskit addon AQC-Tensor
In questo notebook, percorreremo i passi di un Qiskit pattern utilizzando la compilazione quantistica approssimata con reti tensoriali (AQC-Tensor) per ottenere una profondità del circuito inferiore rispetto a quella normalmente necessaria per eseguire l'evoluzione di Trotter.
Questi sono i passi che seguiremo:
- Passo 1: Mappatura sul problema quantistico
- Inizializzare l'Hamiltoniano del problema e gli osservabile/i
- Generare uno stato target di rete tensoriale per la porzione iniziale del circuito
- Generare un circuito a bassa profondità che approssima la porzione da comprimere
- Generare un ansatz generale da quel circuito
- Ottimizzare i parametri per avvicinare il più possibile l'ansatz al target
- Aggiungere i passi di Trotter successivi all'ansatz ottimizzato
- Passo 2: Ottimizzare per l'hardware target
- Transpilare il circuito per l'hardware
- Passo 3: Eseguire gli esperimenti
- Utilizzare un backend fittizio per semplicità
- Passo 4: Ricostruire i risultati
- N/A; invece, ci limitiamo a restituire l'osservabile misurato
Passo 1: Mappatura sul circuito quantistico e sull'operatore
Configurare un Hamiltoniano modello e un osservabile
In questo notebook, utilizziamo il modello di Ising su un cerchio di 10 siti:
dove le condizioni al contorno periodiche implicano che per si ottiene , è la costante di accoppiamento tra due siti e è il campo magnetico esterno.
# Added by doQumentation — required packages for this notebook
!pip install -q qiskit qiskit-addon-aqc-tensor qiskit-addon-utils qiskit-ibm-runtime quimb scipy
from qiskit.transpiler import CouplingMap
from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian
# Generate some coupling map to use for this example
coupling_map = CouplingMap.from_heavy_hex(3, bidirectional=False)
# Choose a 10-qubit circle on this coupling map
reduced_coupling_map = coupling_map.reduce([0, 13, 1, 14, 10, 16, 4, 15, 3, 9])
# Get a qubit operator describing the Ising field model
hamiltonian = generate_xyz_hamiltonian(
reduced_coupling_map,
coupling_constants=(0.0, 0.0, 1.0),
ext_magnetic_field=(0.4, 0.0, 0.0),
)
L'osservabile che misureremo è la magnetizzazione totale.
from qiskit.quantum_info import SparsePauliOp
L = reduced_coupling_map.size()
observable = SparsePauliOp.from_sparse_list([("Z", [i], 1 / L / 2) for i in range(L)], num_qubits=L)
Determinare quanta parte dell'evoluzione temporale simulare classicamente
Il nostro obiettivo complessivo è simulare l'evoluzione temporale dell'Hamiltoniano modello sopra indicato. Lo facciamo tramite l'evoluzione di Trotter, che suddividiamo in due parti:
- Una porzione iniziale simulabile con stati prodotto a matrice (MPS). "Compileremo" questa porzione usando AQC come presentato in https://arxiv.org/abs/2301.08609.
- Una porzione successiva del circuito che verrà eseguita sull'hardware. Pianifichiamo di usare AQC-Tensor per comprimere il circuito di evoluzione temporale fino al tempo , quindi di evolvere con normali passi di Trotter fino a .
Generare i circuiti prima e dopo la suddivisione
Ora che abbiamo scelto di dividere a , genereremo due circuiti:
- Un circuito "target" per la porzione AQC dell'evoluzione, da a . Poiché questo viene simulato da un simulatore di reti tensoriali, il numero di strati influisce sul tempo di esecuzione solo di un fattore costante, quindi è opportuno usare un numero generoso di strati per minimizzare l'errore di Trotter.
from qiskit.synthesis import SuzukiTrotter
from qiskit_addon_utils.problem_generators import generate_time_evolution_circuit
aqc_evolution_time = 4.0
aqc_target_num_trotter_steps = 45
aqc_target_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),
time=aqc_evolution_time,
)
- Un circuito di evoluzione successiva, che evolve da a . Poiché questo viene eseguito su hardware quantistico, è preferibile usare il minor numero possibile di strati di Trotter.
subsequent_evolution_time = 1.0
subsequent_num_trotter_steps = 5
subsequent_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=subsequent_num_trotter_steps),
time=subsequent_evolution_time,
)
A fini di confronto successivo, generiamo anche un terzo circuito: uno che evolve per aqc_evolution_time ma che ha lo stesso tempo di evoluzione per passo di Trotter del circuito successivo. Questo è il circuito con cui avremmo lavorato se non avessimo usato un numero generoso di passi di Trotter per il circuito target. Lo chiameremo circuito di confronto.
aqc_comparison_num_trotter_steps = int(
subsequent_num_trotter_steps / subsequent_evolution_time * aqc_evolution_time
)
aqc_comparison_num_trotter_steps
20
comparison_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_comparison_num_trotter_steps),
time=aqc_evolution_time,
)
Generare un ansatz e i parametri iniziali da un circuito di Trotter con meno passi
Per prima cosa, costruiamo un circuito "buono" che ha lo stesso tempo di evoluzione del circuito target, ma con meno passi di 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 elementi:
- un ansatz parametrizzato generale con la stessa connettività a due qubit del circuito in ingresso; e,
- i parametri che, inseriti nell'ansatz, producono il circuito in ingresso (buono).
A breve prenderemo questi parametri e li regoleremo in modo iterativo per avvicinare il più possibile il circuito ansatz al target MPS.
from qiskit_addon_aqc_tensor import generate_ansatz_from_circuit
aqc_ansatz_num_trotter_steps = 5
aqc_good_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),
time=aqc_evolution_time,
)
aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(
aqc_good_circuit, qubits_initially_zero=True
)
aqc_ansatz.draw("mpl", fold=-1)

print(f"Comparison circuit: depth {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")
Comparison circuit: depth 120
Target circuit: depth 270
Ansatz circuit: depth 23, with 515 parameters
Scegliere le impostazioni per la simulazione della rete tensoriale
Qui utilizziamo il simulatore di reti tensoriali basato su quimb. In questo esempio, usiamo il simulatore di stati prodotto a matrice (MPS) di quimb, e utilizziamo JAX per la differenziazione automatica. Consulta la documentazione API per maggiori informazioni su come usare il simulatore quimb.
from functools import partial
import quimb.tensor
from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator
simulator_settings = QuimbSimulator(
partial(quimb.tensor.CircuitMPS, max_bond=100, cutoff=1e-8),
autodiff_backend="jax",
)
Costruire la rappresentazione a stato prodotto a matrice dello stato target AQC
Successivamente, costruiamo una rappresentazione a prodotto di matrici dello stato da approssimare tramite AQC.
from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit
aqc_target_mps = tensornetwork_from_circuit(aqc_target_circuit, simulator_settings)
Nota che, poiché abbiamo scelto un numero generoso di passi di Trotter per lo stato target, questo ha in realtà meno errore di Trotter rispetto al circuito di confronto. Possiamo calcolare la fedeltà () dello stato preparato dal circuito di confronto rispetto allo stato target:
from qiskit_addon_aqc_tensor.simulation import compute_overlap
comparison_mps = tensornetwork_from_circuit(comparison_circuit, simulator_settings)
comparison_fidelity = abs(compute_overlap(comparison_mps, aqc_target_mps)) ** 2
comparison_fidelity
0.9996761790297157
Ottimizzare i parametri dell'ansatz usando calcoli MPS
Qui minimizziamo la funzione di costo più semplice possibile, MaximizeStateFidelity, utilizzando l'ottimizzatore L-BFGS di scipy.
Scegliamo un punto di arresto per la fedeltà in modo che sia superiore a quella che avrebbe avuto il circuito di confronto senza usare AQC. Una volta raggiunto questo punto, il circuito compresso ha meno errore di Trotter e meno profondità rispetto al circuito originale. Con più tempo di elaborazione, è possibile eseguire ulteriori passi di ottimizzazione per aumentare ancora la fedeltà.
from scipy.optimize import OptimizeResult, minimize
from qiskit_addon_aqc_tensor.objective import MaximizeStateFidelity
objective = MaximizeStateFidelity(aqc_target_mps, aqc_ansatz, simulator_settings)
stopping_point = 1 - comparison_fidelity
def callback(intermediate_result: OptimizeResult):
print(f"Intermediate result: Fidelity {1 - intermediate_result.fun:.8}")
if intermediate_result.fun < stopping_point:
# Good enough for now
raise StopIteration
result = minimize(
objective.loss_function,
aqc_initial_parameters,
method="L-BFGS-B",
jac=True,
options={"maxiter": 100},
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
Intermediate result: Fidelity 0.95080335
Intermediate result: Fidelity 0.98408927
Intermediate result: Fidelity 0.99140876
Intermediate result: Fidelity 0.9951876
Intermediate result: Fidelity 0.99563147
Intermediate result: Fidelity 0.99646297
Intermediate result: Fidelity 0.99679298
Intermediate result: Fidelity 0.99715793
Intermediate result: Fidelity 0.99756604
Intermediate result: Fidelity 0.99804283
Intermediate result: Fidelity 0.99832283
Intermediate result: Fidelity 0.99856583
Intermediate result: Fidelity 0.99868698
Intermediate result: Fidelity 0.998867
Intermediate result: Fidelity 0.99902237
Intermediate result: Fidelity 0.99912174
Intermediate result: Fidelity 0.99919705
Intermediate result: Fidelity 0.99926724
Intermediate result: Fidelity 0.99938605
Intermediate result: Fidelity 0.99951297
Intermediate result: Fidelity 0.99956172
Intermediate result: Fidelity 0.99962274
Intermediate result: Fidelity 0.99963919
Intermediate result: Fidelity 0.99967423
Intermediate result: Fidelity 0.9997101
Done after 25 iterations.
Costruire il circuito finale da passare al transpiler
final_circuit = aqc_ansatz.assign_parameters(aqc_final_parameters)
final_circuit.compose(subsequent_circuit, inplace=True)
final_circuit.draw("mpl", fold=-1)

Passo 2: Transpilare per l'esecuzione sull'hardware target
Nel Passo 2 di un Qiskit pattern, transpiliamo questo circuito e gli osservabili desiderati per l'esecuzione su un dispositivo target. Qui utilizziamo un backend fittizio fornito da qiskit-ibm-runtime.
from qiskit import transpile
from qiskit_ibm_runtime.fake_provider import FakeMelbourneV2
backend = FakeMelbourneV2()
isa_circuit = transpile(final_circuit, backend)
isa_observable = observable.apply_layout(isa_circuit.layout)
Il circuito ISA risultante può quindi essere inviato per l'esecuzione sul backend (passo 3 di un Qiskit pattern).
Passo 3: Eseguire sull'hardware quantistico
from qiskit_ibm_runtime import EstimatorV2 as Estimator
estimator = Estimator(backend)
job = estimator.run([(isa_circuit, isa_observable)])
pub_result = job.result()[0]
Passo 4: Ricostruire
La ricostruzione non è necessaria nel nostro caso. Possiamo semplicemente osservare il risultato.
pub_result.data.evs[()]
np.float64(0.047998046875000006)