Funzioni di costo
In questa lezione impareremo a valutare una funzione di costo :
- Per prima cosa, vedremo le primitive di " IBM Quantum "
- Definire una funzione di costo . Si tratta di una funzione specifica del problema che definisce l'obiettivo del problema che l'ottimizzatore deve minimizzare (o massimizzare)
- Definizione di una strategia di misurazione utilizzando le primitive di IBM Quantum per ottimizzare il rapporto tra velocità e precisione
Primitivi
Tutti i sistemi fisici, sia classici che quantistici, possono esistere in diversi stati. Ad esempio, un'auto su strada può avere una certa massa, posizione, velocità o accelerazione che ne caratterizzano lo stato. Allo stesso modo, anche i sistemi quantistici possono avere diverse configurazioni o stati, ma differiscono dai sistemi classici per il modo in cui trattiamo le misure e l'evoluzione dello stato. Questo porta a proprietà uniche, come la sovrapposizione e l' entanglement, che sono esclusive della meccanica quantistica. Così come possiamo descrivere lo stato di un'automobile usando proprietà fisiche come la velocità o l'accelerazione, possiamo anche descrivere lo stato di un sistema quantistico usando osservabili, che sono oggetti matematici.
In meccanica quantistica, gli stati sono rappresentati da vettori colonna complessi normalizzati, o ket ( ), e le osservabili sono operatori lineari hermitiani ( ) che agiscono sui ket. Un autovettore ( ) di un'osservabile è noto come autostato. Misurando un'osservabile per uno dei suoi autostati ( ) si ottiene come lettura l'autovalore corrispondente ( ).
Se vi state chiedendo come misurare un sistema quantistico e cosa potete misurare, Qiskit offre due Operazioni di base e fondamentali o tipo di dati. Qiskit dispone delle primitive Sampler ed Estimator, che servono come elementi costitutivi per costruire facilmente carichi di lavoro algoritmici complessi. che possono aiutarvi:
Sampler: Dato uno stato quantistico , questa primitiva ottiene la probabilità di ogni possibile stato base computazionale.Estimator: Data un'osservabile quantistica e uno stato , questa primitiva calcola il valore atteso di .
Il primitivo Sampler
La primitiva Sampler calcola la probabilità di ottenere ogni possibile stato dalla base computazionale, dato un circuito quantistico che prepara lo stato . Calcola
Dove è il numero di qubit e la rappresentazione intera di ogni possibile stringa binaria in uscita (cioè, numeri interi in base ).
L' IBM Quantum Sampler esegue più volte il circuito su un dispositivo quantistico, effettuando misurazioni ad ogni esecuzione e ricostruendo la distribuzione di probabilità a partire dalle stringhe di bit recuperate. Più cicli (o tentativi ) viene eseguito, più accurati saranno i risultati, ma ciò richiede più tempo e risorse quantistiche.
Tuttavia, poiché il numero di uscite possibili cresce esponenzialmente con il numero di qubit (cioè ), anche il numero di scatti dovrà crescere esponenzialmente per catturare una distribuzione di probabilità densa. Pertanto, Sampler è efficiente solo per distribuzioni di probabilità rade; dove lo stato target deve essere esprimibile come una combinazione lineare degli stati della base computazionale, con un numero di termini che cresce al massimo in modo polinomiale con il numero di qubit:
Il sito Sampler può anche essere configurato per recuperare le probabilità da una sottosezione del circuito, che rappresenta un sottoinsieme degli stati totali possibili.
Il primitivo Estimator
La primitiva Estimator calcola il valore di aspettativa di un'osservabile per uno stato quantistico ; dove le probabilità dell'osservabile possono essere espresse come , essendo gli autovalori dell'osservabile . Il valore di aspettativa è quindi definito come la media di tutti i possibili esiti (cioè gli autovalori dell'osservabile) di una misura dello stato , ponderata per le probabilità corrispondenti:
Tuttavia, non è sempre possibile calcolare il valore atteso di un osservabile, poiché spesso non ne conosciamo la base propria. L' IBM Quantum Estimator utilizza un complesso processo algebrico per stimare il valore atteso su un dispositivo quantistico reale, scomponendo l'osservabile in una combinazione di altri osservabili di cui conosciamo la base propria.
In termini più semplici, Estimator scompone qualsiasi osservabile che non sa come misurare in osservabili più semplici e misurabili, chiamati Insieme di matrici comunemente utilizzate nell'informatica quantistica per rappresentare e manipolare gli stati quantistici, composto dalla matrice identità e dalle tre matrici di Pauli (X, Y e Z)..
Qualsiasi operatore può essere espresso come una combinazione di operatori di Pauli.
Tale che
dove è il numero di qubit, per (cioè la base dei numeri interi ), e .
Dopo aver eseguito questa decomposizione, Estimator ricava un nuovo circuito per ogni osservabile (dal circuito originale), per diagonalizzare effettivamente l'osservabile di Pauli nella base computazionale e misurarla. Possiamo misurare facilmente le osservabili di Pauli perché conosciamo in anticipo, cosa che non avviene in generale per le altre osservabili.
Per ogni circuito, Estimator esegue il circuito corrispondente su un dispositivo quantistico più volte, misura lo stato di uscita nella base computazionale e calcola la probabilità di ottenere ogni possibile uscita . Quindi cerca l'autovalore di corrispondente a ogni uscita , moltiplica per e somma tutti i risultati per ottenere il valore atteso dell'osservabile per il dato stato .
Poiché il calcolo del valore di aspettativa di Pauli è poco pratico (cioè cresce esponenzialmente), Estimator può essere efficiente solo quando una grande quantità di è nulla (cioè, decomposizione Pauli rada invece che densa ). Formalmente diciamo che, affinché questo calcolo sia risolvibile in modo efficiente, il numero di termini non nulli deve crescere al massimo polinomialmente con il numero di qubit :
Il lettore può notare l'assunzione implicita che anche la probabilità Il processo di misurazione di una o più cose. deve essere efficiente come spiegato per Sampler, il che significa che
Esempio guidato per calcolare i valori attesi
Assumiamo lo stato a singolo qubit , e l'osservabile
con il seguente valore di aspettativa teorico
Poiché non sappiamo come misurare questa osservabile, non possiamo calcolare direttamente il suo valore di aspettativa e dobbiamo riesprimerlo come . Si può dimostrare che il risultato è lo stesso, notando che , e .
Vediamo come calcolare direttamente e . Poiché e non commutano (cioè non condividono la stessa autobase), non possono essere misurati simultaneamente, quindi abbiamo bisogno dei circuiti ausiliari:
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
# The following code will work for any other initial single-qubit state and observable
original_circuit = QuantumCircuit(1)
original_circuit.h(0)
H = SparsePauliOp(["X", "Z"], [2, -1])
aux_circuits = []
for pauli in H.paulis:
aux_circ = original_circuit.copy()
aux_circ.barrier()
if str(pauli) == "X":
aux_circ.h(0)
elif str(pauli) == "Y":
aux_circ.sdg(0)
aux_circ.h(0)
else:
aux_circ.id(0)
aux_circ.measure_all()
aux_circuits.append(aux_circ)
original_circuit.draw("mpl")Output:
# Auxiliary circuit for X
aux_circuits[0].draw("mpl")Output:
# Auxiliary circuit for Z
aux_circuits[1].draw("mpl")Output:
Possiamo ora eseguire il calcolo manualmente utilizzando Sampler e verificare i risultati su Estimator:
from qiskit.primitives import StatevectorSampler, StatevectorEstimator
from qiskit.result import QuasiDistribution
import numpy as np
## SAMPLER
shots = 10000
sampler = StatevectorSampler()
job = sampler.run(aux_circuits, shots=shots)
# Run the sampler job and step through results
expvals = []
for index, pauli in enumerate(H.paulis):
data_pub = job.result()[index].data
bitstrings = data_pub.meas.get_bitstrings()
counts = data_pub.meas.get_counts()
quasi_dist = QuasiDistribution(
{outcome: freq / shots for outcome, freq in counts.items()}
)
# Use the probabilities and known eigenvalues of Pauli operators to estimate
# the expectation value.
val = 0
if str(pauli) == "X":
val += -1 * quasi_dist.get(1, 0)
val += 1 * quasi_dist.get(0, 0)
if str(pauli) == "Y":
val += -1 * quasi_dist.get(1, 0)
val += 1 * quasi_dist.get(0, 0)
if str(pauli) == "Z":
val += 1 * quasi_dist.get(0, 0)
val += -1 * quasi_dist.get(1, 0)
expvals.append(val)
# Print expectation values
print("Sampler results:")
for pauli, expval in zip(H.paulis, expvals):
print(f" >> Expected value of {str(pauli)}: {expval:.5f}")
total_expval = np.sum(H.coeffs * expvals).real
print(f" >> Total expected value: {total_expval:.5f}")
# Use estimator for comparison
observables = [
*H.paulis,
H,
] # Note: run for individual Paulis as well as full observable H
estimator = StatevectorEstimator()
job = estimator.run([(original_circuit, observables)])
estimator_expvals = job.result()[0].data.evs
# Print results
print("Estimator results:")
for obs, expval in zip(observables, estimator_expvals):
if obs is not H:
print(f" >> Expected value of {str(obs)}: {expval:.5f}")
else:
print(f" >> Total expected value: {expval:.5f}")Output:
Sampler results:
>> Expected value of X: 1.00000
>> Expected value of Z: 0.00420
>> Total expected value: 1.99580
Estimator results:
>> Expected value of X: 1.00000
>> Expected value of Z: 0.00000
>> Total expected value: 2.00000
Rigore matematico (facoltativo)
Esprimendo rispetto alla base degli autostati di , , si ottiene quanto segue:
Poiché non conosciamo gli autovalori o gli autostati dell'osservabile target , dobbiamo prima considerare la sua diagonalizzazione. Dato che è Un'Hermitiana è una matrice quadrata che è uguale alla sua trasposizione coniugata, o un operatore lineare che è autoaggiunto., esiste una trasformazione unitaria tale che dove è la matrice diagonale degli autovalori, quindi se , e .
Ciò implica che il valore atteso può essere riscritto come:
Dato che se un sistema si trova nello stato la probabilità di misurare è , il valore atteso di cui sopra può essere espresso come segue:
È molto importante notare che le probabilità sono prese dallo stato invece che da . Per questo motivo la matrice è assolutamente necessaria.
Ci si potrebbe chiedere come ottenere la matrice e gli autovalori . Se si disponesse già degli autovalori, non ci sarebbe bisogno di usare un computer quantistico, poiché l'obiettivo degli algoritmi variazionali è trovare gli autovalori di .
Fortunatamente, esiste un modo per aggirare questo problema: qualsiasi matrice può essere scritta come combinazione lineare di prodotti tensoriali di matrici di Pauli e identità, tutte sia ermitiane che unitarie con e noti. Questo è ciò che fa Estimator internamente il IBM Quantum, scomponendo qualsiasi oggetto Operator in un SparsePauliOp.
Ecco gli operatori che possono essere utilizzati:
Riscriviamo quindi rispetto alle identità e alle Paulis:
dove per (cioè la base ), e :
dove e , tali che:
Funzioni di costo
In generale, le funzioni di costo vengono utilizzate per descrivere l'obiettivo di un problema e la performance di uno stato di prova rispetto a tale obiettivo. Questa definizione può essere applicata a vari esempi in chimica, apprendimento automatico, finanza, ottimizzazione e così via.
Consideriamo un semplice esempio di ricerca dello stato fondamentale di un sistema. Il nostro obiettivo è minimizzare il valore di aspettativa dell'osservabile che rappresenta l'energia (hamiltoniana ):
Possiamo usare Estimator per valutare il valore di aspettativa e passare questo valore a un ottimizzatore per minimizzarlo. Se l'ottimizzazione ha successo, restituirà un insieme di valori ottimali dei parametri , dai quali potremo costruire lo stato di soluzione proposto e calcolare il valore di aspettativa osservato come .
Si noti che saremo in grado di minimizzare la funzione di costo solo per l'insieme limitato di stati che stiamo considerando. Questo ci porta a due possibilità distinte:
- Il nostro ansatz non definisce lo stato della soluzione nello spazio di ricerca : Se questo è il caso, il nostro ottimizzatore non troverà mai la soluzione e dobbiamo sperimentare altri ansatz che potrebbero rappresentare il nostro spazio di ricerca in modo più accurato.
- Il nostro ottimizzatore non è in grado di trovare questa soluzione valida : L'ottimizzazione può essere definita globalmente e localmente. Analizzeremo il significato di questo aspetto nella sezione successiva.
In definitiva, eseguiremo un ciclo di ottimizzazione classico, ma affidando la valutazione della funzione di costo a un computer quantistico. Da questo punto di vista, si potrebbe pensare all'ottimizzazione come a un'impresa puramente classica in cui si chiamano alcuni Un ipotetico dispositivo o componente software che svolge una funzione specifica, ma di cui non si conosce il funzionamento interno. L'utente è a conoscenza solo degli ingressi e delle uscite della scatola nera e non sa come la scatola nera elabora gli ingressi per produrre le uscite. ogni volta che l'ottimizzatore deve valutare la funzione di costo.
def cost_func_vqe(params, circuit, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
pub = (circuit, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
return costfrom qiskit.circuit.library import TwoLocal
observable = SparsePauliOp.from_list([("XX", 1), ("YY", -3)])
reference_circuit = QuantumCircuit(2)
reference_circuit.x(0)
variational_form = TwoLocal(
2,
rotation_blocks=["rz", "ry"],
entanglement_blocks="cx",
entanglement="linear",
reps=1,
)
ansatz = reference_circuit.compose(variational_form)
theta_list = (2 * np.pi * np.random.rand(1, 8)).tolist()
ansatz.decompose().draw("mpl")Output:
Per prima cosa, utilizzeremo un simulatore: StatevectorEstimator. Questo è solitamente consigliabile per il debug, ma seguiremo immediatamente l'esecuzione del debug con un calcolo su un vero hardware quantistico. Sempre più spesso i problemi di interesse non sono più simulabili classicamente senza strutture di supercalcolo all'avanguardia.
estimator = StatevectorEstimator()
cost = cost_func_vqe(theta_list, ansatz, observable, estimator)
print(cost)Output:
[-0.58744589]
Ora procederemo con l'esecuzione su un vero computer quantistico. Si notino le modifiche alla sintassi. Le operazioni relative a pass_manager saranno illustrate più nel dettaglio nel prossimo esempio. Una fase di particolare importanza negli algoritmi variazionali è l'utilizzo di una sessione del servizio di calcolo " IBM Quantum ". L'avvio di una sessione consente di eseguire più iterazioni di un algoritmo variazionale senza dover attendere in una nuova coda ogni volta che i parametri vengono aggiornati. Questo è importante se i tempi di attesa sono lunghi e/o sono necessarie molte iterazioni. Si noti che gli utenti di Open Plan non possono utilizzare le sessioni di Quantum Compute. Se non hai accesso alle sessioni, puoi ridurre il numero di iterazioni da inviare in un dato momento e salvare i parametri più recenti per utilizzarli nelle esecuzioni future. Se si inviano troppe iterazioni o si riscontrano tempi di attesa in coda eccessivamente lunghi, potrebbe comparire il codice di errore 1217, che indica lunghi ritardi tra l'invio di un lavoro e l'altro.
# Estimated usage: < 1 min. Benchmarked at 7 seconds on an Eagle processor
# Load necessary packages:
from qiskit_ibm_runtime import (
QiskitRuntimeService,
Session,
EstimatorOptions,
EstimatorV2 as Estimator,
)
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
# Select the least busy backend:
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
)
# Or get a specific backend:
# backend = service.backend("ibm_brisbane")
# Use a pass manager to transpile the circuit and observable for the specific backend being used:
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_ansatz = pm.run(ansatz)
isa_observable = observable.apply_layout(layout=isa_ansatz.layout)
# Set estimator options
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
# Open a Quantum Compute session:
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
cost = cost_func_vqe(theta_list, isa_ansatz, isa_observable, estimator)
session.close()
print(cost)Si noti che i valori ottenuti dai due calcoli precedenti sono molto simili. Le tecniche per migliorare i risultati saranno discusse più avanti.
Esempio di mappatura su sistemi non fisici
Il problema del taglio massimo (max-cut) è un problema di ottimizzazione combinatoria che consiste nel suddividere i vertici di un grafo in due insiemi disgiunti in modo tale da massimizzare il numero di spigoli tra i due insiemi. Più formalmente, dato un grafo non orientato , dove è l'insieme dei vertici e è l'insieme dei bordi, il problema del taglio massimo consiste nel suddividere i vertici in due sottoinsiemi disgiunti, e , in modo tale da massimizzare il numero di bordi con un'estremità in e l'altra in .
È possibile applicare il metodo max-cut per risolvere una serie di problemi, tra cui: il clustering, la progettazione di reti, le transizioni di fase e così via. Inizieremo creando un grafico del problema:
import rustworkx as rx
from rustworkx.visualization import mpl_draw
n = 4
G = rx.PyGraph()
G.add_nodes_from(range(n))
# The edge syntax is (start, end, weight)
edges = [(0, 1, 1.0), (0, 2, 1.0), (0, 3, 1.0), (1, 2, 1.0), (2, 3, 1.0)]
G.add_edges_from(edges)
mpl_draw(
G, pos=rx.shell_layout(G), with_labels=True, edge_labels=str, node_color="#1192E8"
)Output:
Questo problema può essere espresso come un problema di ottimizzazione binaria. Per ogni nodo , dove è il numero di nodi del grafo (in questo caso ), considereremo la variabile binaria . Questa variabile avrà il valore se il nodo è uno dei gruppi che etichetteremo e se è nell'altro gruppo, che etichetteremo come . Indicheremo inoltre come (elemento della matrice di adiacenza ) il peso del bordo che va dal nodo al nodo . Poiché il grafo non è diretto, . Possiamo quindi formulare il nostro problema come una massimizzazione della seguente funzione di costo:
Per risolvere questo problema con un computer quantistico, esprimeremo la funzione di costo come valore atteso di un'osservabile. Tuttavia, le osservabili che Qiskit ammette nativamente consistono in operatori di Pauli, che hanno autovalori e invece di e . Per questo motivo, si procederà al seguente cambio di variabile:
Dove . Possiamo usare la matrice di adiacenza per accedere comodamente ai pesi di tutti i bordi. Questo verrà utilizzato per ottenere la nostra funzione di costo:
Ciò implica che:
Quindi la nuova funzione di costo che vogliamo massimizzare è:
Inoltre, la tendenza naturale di un computer quantistico è quella di trovare i minimi (di solito l'energia più bassa) anziché i massimi, quindi invece di massimizzare , minimizzeremo:
Ora che abbiamo una funzione di costo da minimizzare le cui variabili possono avere i valori e , possiamo fare la seguente analogia con il Pauli :
In altre parole, la variabile sarà equivalente a un gate che agisce sul qubit . Inoltre:
Allora l'osservabile che stiamo per considerare è:
a cui dovremo aggiungere successivamente il termine indipendente:
L'operatore è una combinazione lineare di termini con operatori Z su nodi collegati da un bordo (ricordiamo che il qubit 0 è il più lontano a destra): . Una volta costruito l'operatore, l'ansatz per l'algoritmo QAOA può essere facilmente costruito utilizzando il circuito QAOAAnsatz dalla libreria di circuiti Qiskit.
from qiskit.circuit.library import QAOAAnsatz
from qiskit.quantum_info import SparsePauliOp
hamiltonian = SparsePauliOp.from_list(
[("IIZZ", 1), ("IZIZ", 1), ("IZZI", 1), ("ZIIZ", 1), ("ZZII", 1)]
)
ansatz = QAOAAnsatz(hamiltonian, reps=2)
# Draw
ansatz.decompose(reps=3).draw("mpl")Output:
# Sum the weights, and divide by 2
offset = -sum(edge[2] for edge in edges) / 2
print(f"""Offset: {offset}""")Output:
Offset: -2.5
Poiché lo stimatore " IBM Quantum " accetta direttamente un ansatz hamiltoniano e parametrizzato e restituisce l'energia necessaria, la funzione di costo per un'istanza QAOA è piuttosto semplice:
def cost_func(params, ansatz, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
pub = (ansatz, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
# cost = estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]
return costimport numpy as np
x0 = 2 * np.pi * np.random.rand(ansatz.num_parameters)
estimator = StatevectorEstimator()
cost = cost_func_vqe(x0, ansatz, hamiltonian, estimator)
print(cost)Output:
1.473098768180865
# Estimated usage: < 1 min, benchmarked at 6 seconds on ibm_osaka, 5-23-24
# Load some necessary packages:
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Session, EstimatorV2 as Estimator
# Select the least busy backend:
backend = service.least_busy(
operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
)
# Or get a specific backend:
# backend = service.backend("ibm_brisbane")
# Use a pass manager to transpile the circuit and observable for the specific backend being used:
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_ansatz = pm.run(ansatz)
isa_hamiltonian = hamiltonian.apply_layout(layout=isa_ansatz.layout)
# Set estimator options
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
# Open a Quantum Compute session:
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
cost = cost_func_vqe(x0, isa_ansatz, isa_hamiltonian, estimator)
# Close session after done
session.close()
print(cost)Output:
1.1120776913677988
Rivedremo questo esempio nelle Applicazioni per esplorare come sfruttare un ottimizzatore per iterare attraverso lo spazio di ricerca. In generale, questo include:
- Sfruttare un ottimizzatore per trovare i parametri ottimali
- Legare i parametri ottimali all'ansatz per trovare gli autovalori
- Tradurre gli autovalori nella definizione del nostro problema
Strategia di misurazione: velocità contro precisione
Come accennato, stiamo usando un computer quantistico rumoroso come oracolo black-box, dove il rumore può rendere i valori recuperati non deterministici, portando a fluttuazioni casuali che, a loro volta, danneggeranno - o addirittura impediranno del tutto - la convergenza di alcuni ottimizzatori verso una soluzione proposta. Si tratta di un problema generale che dobbiamo affrontare man mano che esploriamo l'utilità quantistica e progrediamo verso il vantaggio quantistico:
Possiamo avvalerci delle opzioni di soppressione e mitigazione degli errori delle primitive dell’ IBM Quantum e per gestire il rumore e massimizzare l’utilità dei computer quantistici odierni.
Soppressione degli errori
Per "soppressione degli errori" si intendono le tecniche utilizzate per ottimizzare e modificare un circuito durante la compilazione, al fine di ridurre al minimo gli errori. Si tratta di una tecnica di gestione degli errori di base che solitamente comporta una classica fase di pre-elaborazione Costi aggiuntivi introdotti da nuove tecniche, rispetto a un'implementazione di base. rispetto al tempo di esecuzione complessivo. Il carico di lavoro comprende la traslazione dei circuiti per l'esecuzione su hardware quantistico tramite:
- Esprimere il circuito utilizzando le porte native disponibili su un sistema quantistico
- Mappatura dei qubit virtuali in qubit fisici
- Aggiunta di SWAP in base ai requisiti di connettività
- Ottimizzazione dei gate 1Q e 2Q
- Aggiunta del disaccoppiamento dinamico ai qubit inattivi per prevenire gli effetti della decoerenza.
Le primitive consentono di utilizzare tecniche di soppressione degli errori impostando l'opzione optimization_level e selezionando le opzioni di transpilazione avanzate. In un corso successivo, approfondiremo diversi metodi di costruzione dei circuiti per migliorare i risultati, ma per la maggior parte dei casi si consiglia di impostare optimization_level=3.
Visualizzeremo il valore dell'ottimizzazione crescente nel processo di trasposizione esaminando un circuito di esempio con un semplice comportamento ideale.
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
theta = Parameter("theta")
qc = QuantumCircuit(2)
qc.x(1)
qc.h(0)
qc.cp(theta, 0, 1)
qc.h(0)
observables = SparsePauliOp.from_list([("ZZ", 1)])
qc.draw("mpl")Output:
Il circuito di cui sopra può produrre valori di aspettativa sinusoidali dell'osservabile data, a condizione di inserire fasi che coprono un intervallo appropriato, come .
## Setup phases
import numpy as np
phases = np.linspace(0, 2 * np.pi, 50)
# phases need to be expressed as a list of lists in order to work
individual_phases = [[phase] for phase in phases]Possiamo usare un simulatore per mostrare l'utilità di una trasposizione ottimizzata. Di seguito torneremo a utilizzare l'hardware reale per dimostrare l'utilità della mitigazione degli errori. Utilizzeremo QiskitRuntimeService per ottenere un backend reale (in questo caso, ibm_brisbane) e useremo AerSimulator per simulare quel backend, compreso il suo comportamento di rumore.
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_aer import AerSimulator
# get a real backend from the runtime service
service = QiskitRuntimeService()
backend = service.backend("ibm_brisbane")
# generate a simulator that mimics the real quantum system with the latest calibration results
backend_sim = AerSimulator.from_backend(backend)Ora possiamo utilizzare un gestore di passaggi per traspilare il circuito nell'“architettura del set di istruzioni” (ISA) del backend. Si tratta di un nuovo requisito introdotto nel servizio di calcolo " IBM Quantum ": tutti i circuiti inviati a un backend devono rispettare i vincoli del target del backend, il che significa che devono essere scritti in base all'ISA del backend, ovvero all'insieme di istruzioni che il dispositivo è in grado di comprendere ed eseguire. Questi vincoli di destinazione sono definiti da fattori quali i gate di base nativi del dispositivo, la connettività dei qubit e, ove pertinente, le specifiche temporali relative agli impulsi e ad altre istruzioni.
Si noti che nel caso in esame, questa operazione verrà eseguita due volte: una volta con optimization_level = 0, e una volta con un livello di ottimizzazione pari a 3. Ogni volta useremo la primitiva Estimator per stimare i valori di aspettativa dell'osservabile a diversi valori di fase.
# Import estimator and specify that we are using the simulated backend:
from qiskit_ibm_runtime import EstimatorV2 as Estimator
estimator = Estimator(mode=backend_sim)
circuit = qc# Use a pass manager to transpile the circuit and observable for the backend being simulated.
# Start with no optimization:
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=0)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
noisy_exp_values = []
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
noisy_exp_values = cost[0]
# Repeat above steps, but now with optimization = 3:
exp_values_with_opt_es = []
pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=3)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
exp_values_with_opt_es = cost[0]Infine, possiamo tracciare i risultati e vediamo che la precisione del calcolo era abbastanza buona anche senza l'ottimizzazione, ma è decisamente migliorata aumentando l'ottimizzazione al livello 3. Si noti che nei circuiti più profondi e complicati, la differenza tra i livelli di ottimizzazione 0 e 3 è probabilmente più significativa. Si tratta di un circuito molto semplice utilizzato come modello giocattolo.
import matplotlib.pyplot as plt
plt.plot(phases, noisy_exp_values, "o", label="opt=0")
plt.plot(phases, exp_values_with_opt_es, "o", label="opt=3")
plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label="ideal")
plt.ylabel("Expectation")
plt.legend()
plt.show()Output:
Mitigazione degli errori
Per "mitigazione degli errori" si intendono le tecniche che consentono agli utenti di ridurre gli errori di circuito modellando il rumore del dispositivo al momento dell'esecuzione. In genere, ciò comporta un sovraccarico di pre-elaborazione quantistica legato all'addestramento del modello e un sovraccarico di post-elaborazione classica volto a mitigare gli errori nei risultati grezzi mediante l'utilizzo del modello generato.
L'opzione resilience_level della primitiva IBM Quantum specifica il livello di resilienza da implementare per far fronte agli errori. Livelli più elevati generano risultati più accurati, ma comportano tempi di elaborazione più lunghi a causa del sovraccarico legato al campionamento quantistico. I livelli di resilienza possono essere utilizzati per configurare il compromesso tra costo e precisione quando si applica la mitigazione degli errori alla query primitiva.
Quando si implementa una qualsiasi tecnica di mitigazione degli errori, ci aspettiamo che il Una deriva sistematica delle grandezze misurate, solitamente causata da errori. nei nostri risultati si riduca rispetto al bias precedente, non mitigato. In alcuni casi, il pregiudizio può addirittura scomparire. Tuttavia, questo ha un costo. Riducendo la distorsione delle quantità stimate, aumenterà la variabilità statistica (cioè la varianza), di cui possiamo tenere conto aumentando ulteriormente il numero di scatti per circuito nel nostro processo di campionamento. Questo comporta un sovraccarico superiore a quello necessario per ridurre la distorsione, quindi non viene eseguito per impostazione predefinita. È possibile aderire facilmente a questo comportamento regolando il numero di colpi per circuito in options.executions.shots, come mostrato nell'esempio seguente.
In questo corso analizzeremo questi modelli di mitigazione degli errori a livello generale, per illustrare le funzionalità di mitigazione degli errori che le primitive di IBM Quantum sono in grado di offrire, senza entrare nei dettagli dell'implementazione.
Estinzione dell'errore di lettura con rotazione (T-REx)
L'estinzione dell'errore di lettura con torsione (T-REx) utilizza una tecnica nota come torsione di Pauli per ridurre il rumore introdotto durante il processo di misurazione quantistica. Questa tecnica non presuppone alcuna forma specifica di rumore, il che la rende molto generale ed efficace.
Flusso di lavoro complessivo:
- Acquisizione dei dati per lo stato zero con capovolgimenti di bit randomizzati (Pauli X prima della misurazione)
- Acquisire i dati per lo stato desiderato (rumoroso) con capovolgimenti di bit randomizzati (Pauli X prima della misurazione)
- Calcolare la funzione speciale per ogni set di dati e dividere.
Si può impostare con options.resilience_level = 1, come mostrato nell'esempio seguente.
Estrapolazione a rumore zero
L'estrapolazione a rumore zero (ZNE) funziona amplificando prima il rumore nel circuito che prepara lo stato quantico desiderato, ottenendo misurazioni per diversi livelli di rumore e utilizzando tali misurazioni per dedurre il risultato senza rumore.
Flusso di lavoro complessivo:
- Amplificare il rumore del circuito per diversi fattori di rumore
- Eseguire ogni circuito amplificato dal rumore
- Estrapolare fino al limite di rumore zero
Possiamo impostarlo con options.resilience_level = 2. Potremmo ottimizzare ulteriormente questo aspetto esaminando una serie di noise_factors, noise_amplifiers, e extrapolators, ma ciò esula dall'ambito di questo corso. Ti invitiamo a provare queste opzioni seguendo le istruzioni riportate qui.
Ogni metodo ha un proprio overhead associato: un compromesso tra il numero di calcoli quantistici necessari (tempo) e l'accuratezza dei risultati:
Utilizzo delle opzioni di Quantum Compute per la mitigazione e la soppressione
Ecco come calcolare un valore atteso utilizzando le funzionalità di mitigazione e soppressione degli errori nel servizio di elaborazione dati " IBM Quantum ". Possiamo utilizzare esattamente lo stesso circuito e la stessa grandezza osservabile di prima, ma questa volta mantenendo il livello di ottimizzazione fissato al livello 2 e regolando la resilienza o le tecniche di mitigazione dell'errore utilizzate. Questo processo di mitigazione degli errori si ripete più volte nel corso di un ciclo di ottimizzazione.
Questa parte viene eseguita su hardware reale, poiché la mitigazione degli errori non è disponibile sui simulatori.
# Estimated usage: 8 minutes, benchmarked on an Eagle processor, 5-23-24
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import (
Session,
EstimatorOptions,
EstimatorV2 as Estimator,
)
# We select the least busy backend
# Select the least busy backend
# backend = service.least_busy(
# operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
# )
# Or use a specific backend
backend = service.backend("ibm_brisbane")
# Initialize some variables to save the results from different runs:
exp_values_with_em0_es = []
exp_values_with_em1_es = []
exp_values_with_em2_es = []
# Use a pass manager to optimize the circuit and observables for the backend chosen:
pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
# Open a session and run with no error mitigation:
estimator_options = EstimatorOptions(resilience_level=0, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em0_es = cost[0]
# Open a session and run with resilience = 1:
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em1_es = cost[0]
# Open a session and run with resilience = 2:
estimator_options = EstimatorOptions(resilience_level=2, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em2_es = cost[0]Come in precedenza, possiamo tracciare i valori di aspettativa risultanti in funzione dell'angolo di fase per i tre livelli di attenuazione dell'errore utilizzati. Con grande difficoltà, si può notare che l'attenuazione degli errori migliora leggermente i risultati. Anche in questo caso, questo effetto è molto più pronunciato nei circuiti più profondi e complicati.
import matplotlib.pyplot as plt
plt.plot(phases, exp_values_with_em0_es, "o", label="unmitigated")
plt.plot(phases, exp_values_with_em1_es, "o", label="resil = 1")
plt.plot(phases, exp_values_with_em2_es, "o", label="resil = 2")
plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label="ideal")
plt.ylabel("Expectation")
plt.legend()
plt.show()Output:
Riepilogo
In questa lezione avete imparato a creare una funzione di costo:
- Creare una funzione di costo
- Come sfruttare le primitive di " IBM Quantum " per mitigare e sopprimere il rumore
- Come definire una strategia di misurazione per ottimizzare la velocità rispetto all'accuratezza
Ecco il nostro carico di lavoro variazionale di alto livello:
La nostra funzione di costo viene eseguita durante ogni iterazione del ciclo di ottimizzazione. La prossima lezione analizzerà come l'ottimizzatore classico utilizza la valutazione della funzione di costo per selezionare nuovi parametri.
import qiskit
import qiskit_ibm_runtime
print(qiskit.version.get_version_info())
print(qiskit_ibm_runtime.version.get_version_info())Output:
1.1.0
0.23.0