Mettere tutto insieme con Qiskit Runtime
Riepilogo​
Victoria Lipinska offre un riepilogo finale di tutto ciò che abbiamo imparato finora.
Riferimenti​
I seguenti articoli vengono citati nel video precedente.
- Quantum Chemistry in the Age of Quantum Computing, Cao, et al.
- Quantum computational chemistry, McArdle, et al.
VQE con i pattern di Qiskit​
Abbiamo tutti i componenti necessari per un calcolo VQE:
- Hamiltoniano
- Ansatz
- Ottimizzatore classico
Ora dobbiamo solo combinarli nel framework dei pattern di Qiskit.
Passo 1: Mappare gli input classici su un problema quantistico​
Come indicato in precedenza, partiremo dal presupposto che un Hamiltoniano di interesse, nel formato appropriato, sia già stato generato. Se hai domande al riguardo, consulta la lezione sugli Hamiltoniani per una guida. Il blocco di codice seguente configura i componenti illustrati nelle lezioni precedenti. Qui abbiamo scelto di modellare H2 perché il suo Hamiltoniano è abbastanza compatto da poter essere scritto per intero.
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-aer qiskit-ibm-runtime scipy
# General imports
import numpy as np
from qiskit.quantum_info import SparsePauliOp
# Hamiltonian obtained from a previous lesson
H = SparsePauliOp(
[
"IIII",
"IIIZ",
"IZII",
"IIZI",
"ZIII",
"IZIZ",
"IIZZ",
"ZIIZ",
"IZZI",
"ZZII",
"ZIZI",
"YYYY",
"XXYY",
"YYXX",
"XXXX",
],
coeffs=[
-0.09820182 + 0.0j,
-0.1740751 + 0.0j,
-0.1740751 + 0.0j,
0.2242933 + 0.0j,
0.2242933 + 0.0j,
0.16891402 + 0.0j,
0.1210099 + 0.0j,
0.16631441 + 0.0j,
0.16631441 + 0.0j,
0.1210099 + 0.0j,
0.17504456 + 0.0j,
0.04530451 + 0.0j,
0.04530451 + 0.0j,
0.04530451 + 0.0j,
0.04530451 + 0.0j,
],
)
nuclear_repulsion = 0.7199689944489797
Selezioniamo un circuito efficient_su2 e l'ottimizzatore COBYLA per iniziare.
# Pre-defined ansatz circuit
from qiskit.circuit.library import efficient_su2
# SciPy minimizer routine
from scipy.optimize import minimize
# Plotting functions
# Random initial state and efficient_su2 ansatz
ansatz = efficient_su2(H.num_qubits, su2_gates=["rx"], entanglement="linear", reps=1)
x0 = 2 * np.pi * np.random.random(ansatz.num_parameters)
print(ansatz.decompose().depth())
ansatz.decompose().draw("mpl")
5
Ora costruiamo la nostra funzione di costo. Questa è ovviamente correlata all'Hamiltoniano, ma se ne distingue perché l'Hamiltoniano è un operatore, mentre noi vogliamo una funzione che restituisca il valore di aspettazione di quell'operatore, usando Estimator. Naturalmente, per farlo si avvale dell'ansatz e dei parametri variazionali, che compaiono quindi tutti come argomenti. Di seguito definiamo versioni leggermente diverse per l'uso su hardware reale o su simulatori.
def cost_func(params, ansatz, H, estimator):
pub = (ansatz, [H], [params])
result = estimator.run(pubs=[pub]).result()
energy = result[0].data.evs[0]
return energy
# def cost_func_sim(params, ansatz, H, estimator):
# energy = estimator.run(ansatz, H, parameter_values=params).result().values[0]
# return energy
Passo 2: Ottimizzare il problema per l'esecuzione quantistica​
Vogliamo che il nostro codice giri nel modo più efficiente possibile sull'hardware che utilizziamo. Dobbiamo quindi selezionare un Backend per avviare la fase di ottimizzazione. Il codice seguente seleziona il Backend meno occupato disponibile.
# To run on hardware, select the backend with the fewest number of jobs in the queue
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService(channel="ibm_quantum_platform")
backend = service.least_busy(operational=True, simulator=False)
backend.name
Ottimizzare il circuito per l'esecuzione su un Backend reale è un argomento ricco e cruciale, ma non è specifico del VQE. Per ora ci limitiamo a ricordare due termini importanti:
- optimization_level: descrive quanto bene il circuito è adattato al layout del Backend selezionato. Il livello di ottimizzazione più basso fa solo il minimo indispensabile per far girare il circuito sul dispositivo: mappa i qubit del circuito sui qubit del dispositivo e aggiunge gate di swap per consentire tutte le operazioni a due qubit. Il livello di ottimizzazione più alto è molto più intelligente e usa numerosi accorgimenti per ridurre il conteggio complessivo dei gate. Poiché i gate multi-qubit hanno alti tassi di errore e i qubit decoerono nel tempo, i circuiti più brevi dovrebbero dare risultati migliori.
- Dynamical Decoupling: possiamo applicare una sequenza di gate ai qubit inattivi. Questo cancella alcune interazioni indesiderate con l'ambiente.
Consulta la documentazione collegata per maggiori informazioni sull'ottimizzazione dei circuiti. Il codice seguente genera un pass manager usando i pass manager preimpostati di
qiskit.transpiler.
from qiskit.transpiler import PassManager
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.transpiler.passes import (
ALAPScheduleAnalysis,
PadDynamicalDecoupling,
ConstrainedReschedule,
)
from qiskit.circuit.library import XGate
target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
pm.scheduling = PassManager(
[
ALAPScheduleAnalysis(target=target),
ConstrainedReschedule(
acquire_alignment=target.acquire_alignment,
pulse_alignment=target.pulse_alignment,
target=target,
),
PadDynamicalDecoupling(
target=target,
dd_sequence=[XGate(), XGate()],
pulse_alignment=target.pulse_alignment,
),
]
)
# Use the pass manager and draw the resulting circuit
ansatz_isa = pm.run(ansatz)
ansatz_isa.draw(output="mpl", idle_wires=False, style="iqp")
Dobbiamo analogamente applicare le caratteristiche del layout del dispositivo all'Hamiltoniano.
hamiltonian_isa = H.apply_layout(ansatz_isa.layout)
hamiltonian_isa
SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYYYII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYXXII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXYYII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXXXII'],
coeffs=[-0.09820182+0.j, -0.1740751 +0.j, -0.1740751 +0.j, 0.2242933 +0.j,
0.2242933 +0.j, 0.16891402+0.j, 0.1210099 +0.j, 0.16631441+0.j,
0.16631441+0.j, 0.1210099 +0.j, 0.17504456+0.j, 0.04530451+0.j,
0.04530451+0.j, 0.04530451+0.j, 0.04530451+0.j])
Passo 3: Eseguire usando i Primitivi di Qiskit​
Prima di eseguire sull'hardware selezionato, è una buona idea usare un simulatore per un debugging preliminare e, a volte, per stime dell'errore. Per questi motivi, mostriamo brevemente come eseguire VQE su un simulatore. È tuttavia fondamentale notare che nessun computer classico, simulatore o GPU è in grado di simulare accuratamente la piena funzionalità di un computer quantistico a 127 qubit fortemente intrecciati. Nell'era attuale dell'utilità quantistica, i simulatori avranno un utilizzo limitato.
Ricorda che per ogni scelta di parametri nel circuito variazionale deve essere calcolato un valore di aspettazione (poiché è quello il valore da minimizzare). Come avrai già intuito, il modo più efficiente per farlo è usare la primitiva di Qiskit, Estimator. Inizieremo usando un simulatore locale, che richiederà la versione locale di Estimator chiamata BackendEstimator.
Mantenendo il Backend reale usato per l'ottimizzazione, possiamo importare un modello del comportamento del rumore di quel dispositivo per poi usarlo con il simulatore locale di nostra scelta. Qui useremo aer_simulator_statevector.
# We will start by using a local simulator
from qiskit_aer import AerSimulator
# Import an estimator, this time from qiskit (we will import from Runtime for real hardware)
from qiskit.primitives import BackendEstimatorV2
# generate a simulator that mimics the real quantum system
backend_sim = AerSimulator.from_backend(backend)
estimator = BackendEstimatorV2(backend=backend_sim)
È finalmente giunto il momento di implementare il VQE, minimizzando la funzione di costo usando l'Hamiltoniano selezionato, l'ansatz, l'ottimizzatore classico e il nostro BackendEstimator, basato sul Backend reale che abbiamo scelto per l'uso successivo. Nota che qui abbiamo scelto un numero relativamente piccolo per le iterazioni massime. Questo perché stiamo usando il simulatore solo per il debugging. I passi di ottimizzazione del VQE richiedono spesso centinaia di iterazioni per convergere.
res = minimize(
cost_func,
x0,
args=(ansatz_isa, hamiltonian_isa, estimator),
method="cobyla",
options={"maxiter": 10, "disp": True},
)
print(getattr(res, "fun") - nuclear_repulsion)
print(res)
Return from COBYLA because the objective function has been evaluated MAXFUN times.
Number of function values = 10 Least value of F = -0.11556938907226563
The corresponding X is:
[4.11796514 4.52126324 0.69570423 4.12781503 6.55507846 1.80713073
0.9645473 6.23812214]
-0.8355383835212453
message: Return from COBYLA because the objective function has been evaluated MAXFUN times.
success: False
status: 3
fun: -0.11556938907226563
x: [ 4.118e+00 4.521e+00 6.957e-01 4.128e+00 6.555e+00
1.807e+00 9.645e-01 6.238e+00]
nfev: 10
maxcv: 0.0
Il codice è stato eseguito correttamente, anche se non ha convergito, come previsto. Procederemo eseguendo il calcolo su hardware reale e poi discuteremo i risultati. Per i Backend reali useremo Qiskit Runtime Estimator. Vorremo eseguire il calcolo all'interno di una sessione Qiskit Runtime e in genere vorremo specificare le opzioni per quella sessione.
from qiskit_ibm_runtime import QiskitRuntimeService, Session
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime.options import EstimatorOptions
Tra le altre cose, usare una sessione significa che il nostro job dovrà aspettare in coda solo una volta, all'inizio. Le iterazioni successive dell'ottimizzatore classico non verranno accodate. Nella sessione possiamo impostare i livelli di resilienza e ottimizzazione. Questi strumenti sono abbastanza importanti da meritare una breve panoramica di ciascuno e della loro importanza nel VQE, con link per approfondire:
- Sessioni Runtime: il VQE è intrinsecamente iterativo, poiché l'ottimizzatore classico seleziona nuovi parametri variazionali, e quindi nuovi gate, a ogni tentativo successivo. Senza l'uso delle sessioni, ciò potrebbe comportare tempo di coda aggiuntivo tra ogni circuito di prova. Racchiudere il calcolo VQE all'interno di una sessione produce una sola coda iniziale prima dell'avvio del job, senza tempo di attesa aggiuntivo tra i passi variazionali. Questa strategia era già stata usata nell'esempio della lezione precedente, ma potrebbe avere un ruolo ancora più importante quando si varia la geometria. Per saperne di più sulle sessioni, consulta la documentazione sulle modalità di esecuzione.
- Ottimizzazione integrata in Estimator: in Estimator sono presenti opzioni integrate per ottimizzare un calcolo. In molti contesti (Estimator incluso), le impostazioni sono limitate a 0 e 1, dove 0 indica nessuna ottimizzazione e 1 (il valore predefinito) indica una certa ottimizzazione del circuito per l'hardware selezionato. In altri contesti sono disponibili impostazioni da 0 a 3. Per maggiori informazioni sui metodi specifici usati nelle diverse impostazioni, consulta la documentazione. Qui imposteremo l'ottimizzazione a 0 e useremo
skip\_transpilation = true, poiché abbiamo già transpilato il nostro circuito usando il pass manager nella sezione di ottimizzazione. - Resilienza integrata in Estimator: come per l'ottimizzazione, Estimator ha impostazioni integrate per la resilienza agli errori, corrispondenti a diversi approcci alla mitigazione degli errori. Per informazioni sulle impostazioni del livello di resilienza, consulta la documentazione.
Vale la pena notare che la mitigazione degli errori gioca un ruolo sfumato nella convergenza di un calcolo VQE. L'ottimizzatore classico cerca nello spazio dei parametri quelli che minimizzano l'energia. Quando si è molto lontani dai parametri ottimali, un gradiente ripido può essere percepibile all'ottimizzatore classico anche in presenza di errori. Ma man mano che il calcolo converge e ci si avvicina ai valori ottimali, il gradiente diventa più piccolo e viene più facilmente annullato dagli errori. Quanta mitigazione degli errori vuoi usare? In quali momenti della convergenza? Queste sono scelte che devi fare per il tuo caso d'uso specifico.
Per questa prima esecuzione su hardware reale, abbiamo impostato la resilienza a 0 per facilitare un'esecuzione relativamente rapida. Per qualsiasi applicazione seria, vorrai usare la mitigazione degli errori. Nota che nella cella seguente ci sono due set di opzioni: (1) le opzioni per la sessione Runtime, che abbiamo chiamato "session_options", e (2) le opzioni per l'ottimizzatore classico, qui semplicemente chiamate "options".
estimator_options = EstimatorOptions(resilience_level=0, default_shots=2000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
res = minimize(
cost_func,
x0,
args=(ansatz_isa, hamiltonian_isa, estimator),
method="cobyla",
options={"maxiter": 10, "disp": True},
)
Return from COBYLA because the objective function has been evaluated MAXFUN times.
Number of function values = 10 Least value of F = -0.11691688904
The corresponding X is:
[5.11796514 5.52126324 0.69570423 5.12781503 6.55507846 1.80713073
1.9645473 6.23812214]
Puoi visualizzare l'avanzamento del tuo job su IBM Quantum® Platform nella sezione Workloads.
print(getattr(res, "fun") - nuclear_repulsion)
print(res)
-0.8368858834889796
message: Return from COBYLA because the objective function has been evaluated MAXFUN times.
success: False
status: 3
fun: -0.11691688904
x: [ 5.118e+00 5.521e+00 6.957e-01 5.128e+00 6.555e+00
1.807e+00 1.965e+00 6.238e+00]
nfev: 10
maxcv: 0.0
Passo 4: Post-elaborazione, restituzione del risultato in formato classico​
Prendiamoci un momento per assicurarci di capire questi output. L'output "fun" è il valore minimo che abbiamo ottenuto per la funzione di costo (non necessariamente l'ultimo valore calcolato). Questa è l'energia totale, inclusa la repulsione nucleare positiva, motivo per cui abbiamo anche definito electron_energy.
Nel caso precedente, abbiamo un messaggio che indica che il numero massimo di valutazioni della funzione è stato superato e che il numero di valutazioni della funzione (nfev) era 10. Questo significa semplicemente che gli altri criteri di convergenza dell'ottimizzazione non sono stati soddisfatti; in altre parole, non c'è motivo di pensare di aver trovato l'energia dello stato fondamentale. Questo è anche il significato di success uguale a "False".
Infine, abbiamo x. Questo è il vettore dei parametri variazionali: i parametri usati nel calcolo che ha prodotto la funzione di costo minima (valore di aspettazione dell'energia). Questi otto valori corrispondono agli otto angoli di rotazione nei gate dell'ansatz che accettano angoli di rotazione variabili.
Congratulazioni! Hai eseguito un calcolo VQE su una QPU IBM Quantum!
Nella prossima lezione vedremo come adattare questo flusso di lavoro per includere variabili nel tuo Hamiltoniano. Nel contesto dei problemi di chimica quantistica, questo potrebbe significare variare la geometria per determinare la forma delle molecole o i siti di legame.
import qiskit
import qiskit_ibm_runtime
print(qiskit.version.get_version_info())
print(qiskit_ibm_runtime.version.get_version_info())
2.1.0
0.40.1