Algoritmi quantistici: algoritmi quantistici variazionali
Takashi Imamichi (24 maggio 2024)
Scarica il PDF della lezione originale. Nota che alcuni frammenti di codice potrebbero risultare deprecati, poiché si tratta di immagini statiche.
Il tempo QPU approssimativo per eseguire questo esperimento è di 9 minuti (testato su un processore Eagle).
(questo notebook potrebbe non completare l'esecuzione nel tempo consentito dal piano Open. Si prega di utilizzare le risorse di quantum computing con giudizio.)
1. Introduzione
Questo tutorial fornisce una panoramica di un algoritmo ibrido quantistico-classico, con particolare attenzione al variational quantum eigensolver (VQE) e al quantum approximate optimization algorithm (QAOA). L'obiettivo principale di questi algoritmi è affrontare problemi di ottimizzazione impiegando circuiti quantistici con gate quantistici parametrizzati.
Nonostante i progressi nel quantum computing, la presenza di rumore nei dispositivi quantistici attuali rende difficile estrarre risultati significativi da circuiti quantistici profondi. Per superare questa sfida, VQE e QAOA adottano un approccio ibrido quantistico-classico, che consiste nell'eseguire iterativamente circuiti quantistici relativamente brevi tramite calcolo quantistico e nell'ottimizzare i parametri dei circuiti quantistici parametrizzati target tramite calcolo classico.
QAOA ha il potenziale di fornire soluzioni ottimali ai problemi target su scala utility, grazie all'applicazione di varie tecniche di error mitigation e suppression. VQE ha molte applicazioni (come la chimica quantistica) in cui è meno scalabile. Tuttavia, sono emersi diversi approcci basati sugli autovalori per completare e potenziare VQE, tra cui la diagonalizzazione del sottospazio di Krylov e la sampling-based quantum diagonalization (SQD). Comprendere VQE è un primo passo fondamentale per comprendere l'ampia gamma di algoritmi ibridi classico-quantistici emersi.
Questo modulo descrive i concetti fondamentali e l'implementazione di VQE e QAOA. Tutorial successivi esploreranno argomenti avanzati e tecniche per scalare questi algoritmi. Per eseguire questo notebook è necessaria la seguente libreria nel tuo ambiente. Se non l'hai ancora installata, puoi farlo decommentando ed eseguendo la cella seguente.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime rustworkx scipy
# % pip install 'qiskit[visualization]' qiskit-ibm-runtime
2. Calcolo dell'autovalore minimo di un semplice Hamiltoniano
Inizieremo applicando VQE a un caso molto semplice, per vedere come funziona. Calcoleremo l'autovalore minimo della matrice di Pauli con VQE. Cominciamo importando alcuni pacchetti generali.
import numpy as np
from qiskit.circuit import ParameterVector, QuantumCircuit
from qiskit.primitives import StatevectorEstimator, StatevectorSampler
from qiskit.quantum_info import SparsePauliOp
from scipy.optimize import minimize
Definiamo ora l'operatore di interesse e visualizziamolo in forma matriciale.
op = SparsePauliOp("Z")
op.to_matrix()
array([[ 1.+0.j, 0.+0.j],
[ 0.+0.j, -1.+0.j]])
È facile ottenere gli autovalori in modo classico, così possiamo verificare il nostro lavoro. Questo potrebbe diventare difficile man mano che ci avviciniamo alla scala utility. Qui usiamo numpy.
# compute eigenvalues with numpy
result = np.linalg.eigh(op.to_matrix())
print("Eigenvalues:", result.eigenvalues)
Eigenvalues: [-1. 1.]
Per ottenere gli autovalori tramite un algoritmo quantistico variazionale, costruiamo un circuito con gate che accettano parametri variazionali:
# define a variational form
param = ParameterVector("a", 3)
qc = QuantumCircuit(1, 1)
qc.u(param[0], param[1], param[2], 0)
qc_estimator = qc.copy()
qc.measure(0, 0)
qc.draw("mpl")
Se vogliamo stimare il valore di aspettazione di un operatore (come ), dobbiamo usare Estimator. Se vogliamo osservare gli stati del sistema, usiamo Sampler.
sampler = StatevectorSampler()
estimator = StatevectorEstimator()
Possiamo calcolare i conteggi delle stringhe di bit 0 e 1 con valori di parametro casuali [1, 2, 3] usando Sampler.
# compute counts of bitstrings with random parameter values by Sampler
result = sampler.run([(qc, [1, 2, 3])]).result()
counts = result[0].data.c.get_counts()
counts
{'0': 783, '1': 241}
Sappiamo che possiamo calcolare il valore di aspettazione di Z tramite con probabilità .
# compute the expectation value of Z based on the counts
(counts.get("0", 0) - counts.get("1", 0)) / sum(counts.values())
0.529296875
Questo circuito ha funzionato, ma i valori dei parametri scelti non corrispondevano a uno stato a bassa energia (o basso autovalore). L'autovalore ottenuto è notevolmente più alto del minimo. Il risultato è simile quando si usa estimator.
Nota che Estimator accetta circuiti quantistici senza misurazioni.
result = estimator.run([(qc_estimator, op, [1, 2, 3])]).result()
result[0].data.evs
array(0.54030231)
Dovremo cercare tra i parametri e trovare quelli che producono l'autovalore più basso. Definiamo una funzione che riceva i valori dei parametri della forma variazionale e restituisca il valore di aspettazione .
# define a cost function to look for the minimum eigenvalue of Z
def cost(x):
result = sampler.run([(qc, x)]).result()
counts = result[0].data.c.get_counts()
expval = (counts.get("0", 0) - counts.get("1", 0)) / sum(counts.values())
# the following line shows the trajectory of the optimization
print(expval, counts)
return expval
Applichiamo la funzione minimize di SciPy per trovare l'autovalore minimo di Z.
# minimize the cost function with scipy's minimize
min_result = minimize(cost, [0, 0, 0], method="COBYLA", tol=1e-8)
min_result
1.0 {'0': 1024}
0.494140625 {'0': 765, '1': 259}
0.466796875 {'0': 751, '1': 273}
0.564453125 {'0': 801, '1': 223}
-0.4296875 {'1': 732, '0': 292}
-0.984375 {'1': 1016, '0': 8}
-0.8984375 {'1': 972, '0': 52}
-0.990234375 {'1': 1019, '0': 5}
-0.892578125 {'1': 969, '0': 55}
-0.986328125 {'1': 1017, '0': 7}
-0.861328125 {'1': 953, '0': 71}
-1.0 {'1': 1024}
-0.982421875 {'1': 1015, '0': 9}
-0.99609375 {'1': 1022, '0': 2}
-0.986328125 {'1': 1017, '0': 7}
-1.0 {'1': 1024}
-0.990234375 {'1': 1019, '0': 5}
-0.998046875 {'1': 1023, '0': 1}
-0.99609375 {'1': 1022, '0': 2}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-0.998046875 {'1': 1023, '0': 1}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-0.998046875 {'1': 1023, '0': 1}
-0.998046875 {'1': 1023, '0': 1}
-0.998046875 {'1': 1023, '0': 1}
-1.0 {'1': 1024}
-0.99609375 {'1': 1022, '0': 2}
-1.0 {'1': 1024}
-0.99609375 {'1': 1022, '0': 2}
-0.998046875 {'1': 1023, '0': 1}
-0.998046875 {'1': 1023, '0': 1}
-0.99609375 {'1': 1022, '0': 2}
-0.998046875 {'1': 1023, '0': 1}
-1.0 {'1': 1024}
-0.998046875 {'1': 1023, '0': 1}
-0.998046875 {'1': 1023, '0': 1}
-0.99609375 {'1': 1022, '0': 2}
-1.0 {'1': 1024}
-0.998046875 {'1': 1023, '0': 1}
-1.0 {'1': 1024}
-0.998046875 {'1': 1023, '0': 1}
-0.998046875 {'1': 1023, '0': 1}
-1.0 {'1': 1024}
-0.998046875 {'1': 1023, '0': 1}
-0.998046875 {'1': 1023, '0': 1}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-0.998046875 {'1': 1023, '0': 1}
-0.994140625 {'1': 1021, '0': 3}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
message: Optimization terminated successfully.
success: True
status: 1
fun: -1.0
x: [ 3.182e+00 1.338e+00 1.664e-01]
nfev: 63
maxcv: 0.0
# check counts of bitstrings with the optimal parameters
result = sampler.run([(qc, min_result.x)]).result()
result[0].data.c.get_counts()
{'0': 1, '1': 1023}
2.1 Esercizio
Calcola l'autovalore minimo di con VQE.
z2 = SparsePauliOp("ZZ")
print(z2)
print(z2.to_matrix())
SparsePauliOp(['ZZ'],
coeffs=[1.+0.j])
[[ 1.+0.j 0.+0.j 0.+0.j 0.+0.j]
[ 0.+0.j -1.+0.j 0.+0.j 0.+0.j]
[ 0.+0.j 0.+0.j -1.+0.j 0.+0.j]
[ 0.+0.j 0.+0.j 0.+0.j 1.+0.j]]
# compute eigenvalues with numpy
# define a variational form
# qc = ...
# compute counts of bitstrings with a random parameter values by Sampler
# result = sampler.run(...)
# result
# compute the expectation value of ZZ based on the counts
# verify the expectation value of ZZ with Estimator
# define a cost function to look for the minimum eigenvalue of ZZ
# def cost(x):
# expval = ...
# return expval
# minimize the cost function with scipy's minimize
# min_result = minimize(cost, [...], method="COBYLA", tol=1e-8)
# min_result
# check counts of bitstrings with the optimal parameter values
# result = sampler.run(qc, min_result.x).result()
# result
Soluzioni dell'esercizio
Definiamo l'operatore di interesse e visualizziamolo in forma matriciale.
z2 = SparsePauliOp("ZZ")
print(z2)
print(z2.to_matrix())
SparsePauliOp(['ZZ'],
coeffs=[1.+0.j])
[[ 1.+0.j 0.+0.j 0.+0.j 0.+0.j]
[ 0.+0.j -1.+0.j 0.+0.j 0.+0.j]
[ 0.+0.j 0.+0.j -1.+0.j 0.+0.j]
[ 0.+0.j 0.+0.j 0.+0.j 1.+0.j]]
Per ottenere gli autovalori tramite un algoritmo quantistico variazionale, costruiamo un circuito con gate che accettano parametri variazionali:
# define a variational form
param = ParameterVector("a", 6)
qc = QuantumCircuit(2, 2)
qc.u(param[0], param[1], param[2], 0)
qc.u(param[3], param[4], param[5], 1)
qc_estimator = qc.copy()
qc.measure([0, 1], [0, 1])
qc.draw("mpl")
Se vogliamo stimare il valore di aspettazione di un operatore (come ), useremmo Estimator. Se vogliamo osservare gli stati del sistema, usiamo Sampler.
sampler = StatevectorSampler()
estimator = StatevectorEstimator()
# compute counts of bitstrings with random parameter values by Sampler
result = sampler.run([(qc, [1, 2, 3, 4, 5, 6])]).result()
counts = result[0].data.c.get_counts()
counts
{'10': 661, '11': 203, '01': 47, '00': 113}
# compute the expectation value of ZZ based on the counts
(
counts.get("00", 0)
- counts.get("01", 0)
- counts.get("10", 0)
+ counts.get("11", 0)
) / sum(counts.values())
-0.3828125
Questo circuito ha funzionato, ma i valori dei parametri scelti non corrispondevano a uno stato a bassa energia (o basso autovalore). L'autovalore ottenuto è notevolmente più alto del minimo. Il risultato è simile quando si usa estimator.
# verify the expectation value of ZZ with Estimator
result = estimator.run([(qc_estimator, z2, [1, 2, 3, 4, 5, 6])]).result()
result[0].data.evs
array(-0.35316516)
Dovremo cercare tra i parametri e trovare quelli che producono l'autovalore più basso.
# define a cost function to look for the minimum eigenvalue of ZZ
def cost(x):
result = sampler.run([(qc, x)]).result()
counts = result[0].data.c.get_counts()
expval = (
counts.get("00", 0)
- counts.get("01", 0)
- counts.get("10", 0)
+ counts.get("11", 0)
) / sum(counts.values())
print(expval, counts)
return expval
# minimize the cost function with scipy's minimize
min_result = minimize(cost, [0, 0, 0, 0, 0, 0], method="COBYLA", tol=1e-8)
min_result
1.0 {'00': 1024}
0.578125 {'00': 808, '01': 216}
0.5234375 {'00': 780, '01': 244}
0.548828125 {'00': 793, '01': 231}
0.3515625 {'00': 637, '10': 164, '11': 55, '01': 168}
0.3359375 {'00': 638, '11': 46, '10': 174, '01': 166}
0.283203125 {'00': 602, '10': 181, '01': 186, '11': 55}
-0.087890625 {'01': 414, '00': 184, '10': 143, '11': 283}
0.236328125 {'10': 27, '11': 623, '01': 364, '00': 10}
-0.0625 {'11': 261, '01': 403, '00': 219, '10': 141}
0.248046875 {'01': 366, '11': 628, '00': 11, '10': 19}
-0.0625 {'10': 145, '11': 254, '01': 399, '00': 226}
0.228515625 {'01': 373, '11': 609, '00': 20, '10': 22}
0.0546875 {'11': 376, '10': 273, '01': 211, '00': 164}
-0.447265625 {'01': 731, '10': 10, '11': 267, '00': 16}
-0.71484375 {'01': 871, '11': 99, '00': 47, '10': 7}
-0.46484375 {'01': 741, '00': 253, '10': 9, '11': 21}
-0.87890625 {'01': 962, '00': 39, '11': 23}
-0.640625 {'00': 176, '01': 837, '11': 8, '10': 3}
-0.88671875 {'01': 966, '00': 41, '11': 17}
-0.994140625 {'01': 1021, '11': 3}
-0.91796875 {'01': 982, '11': 35, '00': 7}
-0.994140625 {'01': 1021, '11': 2, '00': 1}
-0.939453125 {'01': 993, '00': 31}
-0.990234375 {'01': 1019, '11': 5}
-0.90234375 {'01': 974, '00': 21, '11': 29}
-0.98046875 {'01': 1014, '11': 10}
-0.994140625 {'01': 1021, '00': 3}
-0.990234375 {'01': 1019, '11': 4, '00': 1}
-0.98828125 {'01': 1018, '11': 6}
-0.990234375 {'01': 1019, '11': 4, '00': 1}
-0.994140625 {'01': 1021, '11': 2, '00': 1}
-0.99609375 {'01': 1022, '11': 2}
-0.998046875 {'01': 1023, '00': 1}
-0.99609375 {'01': 1022, '00': 2}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '00': 1}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '00': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '00': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '00': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '00': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-0.99609375 {'01': 1022, '00': 1, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '00': 1}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-0.99609375 {'01': 1022, '11': 1, '00': 1}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '00': 1}
-0.994140625 {'01': 1021, '00': 3}
-0.998046875 {'01': 1023, '00': 1}
-0.99609375 {'01': 1022, '11': 2}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '00': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-0.99609375 {'01': 1022, '11': 2}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
message: Optimization terminated successfully.
success: True
status: 1
fun: -0.998046875
x: [ 3.167e+00 6.940e-01 1.033e+00 -2.894e-02 8.933e-01
1.885e+00]
nfev: 128
maxcv: 0.0
message: Optimization terminated successfully.
success: True
status: 1
fun: -0.99609375
x: [ 3.098e+00 -5.402e-01 1.091e+00 -1.004e-02 3.615e-01
6.913e-01]
nfev: 115
maxcv: 0.0
Abbiamo ottenuto un autovalore estremamente vicino al minimo fornito da numpy.
# check counts of bitstrings with the optimal parameters
result = sampler.run([(qc, min_result.x)]).result()
result[0].data.c.get_counts()
{'01': 1024}
3. Ottimizzazione quantistica con i pattern Qiskit
In questo how-to impareremo i pattern Qiskit e l'ottimizzazione approssimata quantistica. Un pattern Qiskit è un insieme intuitivo e ripetibile di passi per implementare un flusso di lavoro nel quantum computing:
Applicheremo i pattern al contesto dell'ottimizzazione combinatoria e mostreremo come risolvere il problema Max-Cut usando il Quantum Approximate Optimization Algorithm (QAOA), un metodo iterativo ibrido (quantistico-classico).
Nota che questa parte su QAOA è basata su "Part 1: Small-scale QAOA" del tutorial Quantum approximate optimization algorithm. Consulta il tutorial per imparare come scalarlo.
3.1 Pattern Qiskit (su piccola scala) per l'ottimizzazione
Questa parte utilizzerà un problema Max-Cut su piccola scala per illustrare i passi necessari per risolvere un problema di ottimizzazione con un computer quantistico.
Il problema Max-Cut è un problema di ottimizzazione difficile da risolvere (più precisamente, è un problema NP-hard) con diverse applicazioni nel clustering, nella scienza delle reti e nella fisica statistica. Questo tutorial considera un grafo di nodi collegati da archi e mira a partizionare i nodi in due insiemi "tagliando" gli archi, in modo da massimizzare il numero di archi tagliati.
Per fornire un contesto prima di mappare questo problema su un algoritmo quantistico, puoi capire meglio come il problema Max-Cut diventi un problema di ottimizzazione combinatoria classica considerando prima la minimizzazione di una funzione
dove l'input è un vettore le cui componenti corrispondono a ciascun nodo del grafo. Poi si vincola ciascuna di queste componenti a essere o (che rappresentano l'essere inclusi o meno nel taglio). Questo esempio su piccola scala usa un grafo con nodi.
Puoi scrivere una funzione di una coppia di nodi che indica se l'arco corrispondente è nel taglio. Per esempio, la funzione vale 1 solo se uno tra e è 1 (cioè l'arco è nel taglio) e zero altrimenti. Il problema di massimizzare gli archi nel taglio può essere formulato come
che può essere riscritto come una minimizzazione nella forma
Il minimo di in questo caso si ottiene quando il numero di archi attraversati dal taglio è massimo. Come si può notare, non c'è ancora nulla che riguardi il quantum computing. È necessario riformulare il problema in qualcosa che un computer quantistico possa comprendere. Inizializza il problema creando un grafo con nodi.
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import rustworkx as rx
from rustworkx.visualization import mpl_draw
n = 5
graph = rx.PyGraph()
graph.add_nodes_from(range(1, n + 1))
edge_list = [
(0, 1, 1.0),
(0, 2, 1.0),
(1, 2, 1.0),
(1, 3, 1.0),
(2, 4, 1.0),
(3, 4, 1.0),
]
graph.add_edges_from(edge_list)
pos = rx.spring_layout(graph, seed=2)
mpl_draw(graph, node_size=600, pos=pos, with_labels=True, labels=str)
3.2 Passo 1. Mappare gli input classici in un problema quantistico
Il primo passo del pattern consiste nel mappare il problema classico (il grafo) in circuit e operatori quantistici. Per fare ciò, ci sono tre passi principali:
- Utilizzare una serie di riformulazioni matematiche per rappresentare il problema con la notazione dei problemi di Ottimizzazione Binaria Quadratica Non Vincolata (QUBO).
- Riscrivere il problema di ottimizzazione come un Hamiltoniano per cui lo stato fondamentale corrisponde alla soluzione che minimizza la funzione di costo.
- Creare un circuito quantistico che preparerà lo stato fondamentale di questo Hamiltoniano tramite un processo simile al quantum annealing.
Nota: Nella metodologia QAOA, si vuole avere un operatore (Hamiltoniano) che rappresenti la funzione di costo dell'algoritmo ibrido, nonché un circuito parametrizzato (Ansatz) che rappresenti gli stati quantistici con le soluzioni candidate al problema. È possibile campionare questi stati candidati e poi valutarli usando la funzione di costo.
Grafo → problema di ottimizzazione
Il primo passo della mappatura è un cambio di notazione. Di seguito viene espresso il problema in notazione QUBO:
dove è una matrice di numeri reali, corrisponde al numero di nodi nel grafo, è il vettore di variabili binarie introdotto sopra, e indica la trasposta del vettore .
Problem name: maxcut
Minimize
2*x_1*x_2 + 2*x_1*x_3 + 2*x_2*x_3 + 2*x_2*x_4 + 2*x_3*x_5 + 2*x_4*x_5 - 2*x_1
- 3*x_2 - 3*x_3 - 2*x_4 - 2*x_5
Subject to
No constraints
Binary variables (5)
x_1 x_2 x_3 x_4 x_5
Problema di ottimizzazione → Hamiltoniano
È quindi possibile riformulare il problema QUBO come un Hamiltoniano (qui, una matrice che rappresenta l'energia di un sistema):
Passi di riformulazione dal problema QAOA all'Hamiltoniano
Per dimostrare come il problema QAOA possa essere riscritto in questo modo, si sostituiscono prima le variabili binarie con un nuovo insieme di variabili tramite
Qui si può vedere che se è , allora deve essere . Quando le vengono sostituite dalle nel problema di ottimizzazione (), si ottiene una formulazione equivalente.
Ora, se definiamo , rimuoviamo il prefattore e il termine costante , arriviamo alle due formulazioni equivalenti dello stesso problema di ottimizzazione.
Qui, dipende da . Nota che per ottenere abbiamo eliminato il fattore 1/4 e un offset costante di che non hanno ruolo nell'ottimizzazione.
Ora, per ottenere una formulazione quantistica del problema, si promuovono le variabili a una matrice di Pauli , come una matrice della forma
Sostituendo queste matrici nel problema di ottimizzazione sopra, si ottiene il seguente Hamiltoniano
Ricorda anche che le matrici sono incorporate nello spazio computazionale del computer quantistico, ovvero uno spazio di Hilbert di dimensione . Pertanto, termini come vanno intesi come il prodotto tensoriale incorporato nello spazio di Hilbert . Per esempio, in un problema con cinque variabili decisionali, il termine va inteso come dove è la matrice identità .
Questo Hamiltoniano è chiamato Hamiltoniano della funzione di costo. Ha la proprietà che il suo stato fondamentale corrisponde alla soluzione che minimizza la funzione di costo . Pertanto, per risolvere il tuo problema di ottimizzazione è ora necessario preparare lo stato fondamentale di (o uno stato con un'alta sovrapposizione con esso) sul computer quantistico. Campionando da questo stato si otterrà, con alta probabilità, la soluzione di .
def build_max_cut_operator(graph: rx.PyGraph) -> tuple[SparsePauliOp, float]:
sp_list = []
constant = 0
for s, t in graph.edge_list():
w = graph.get_edge_data(s, t)
sp_list.append(("ZZ", [s, t], w / 2))
constant -= 1 / 2
return SparsePauliOp.from_sparse_list(
sp_list, num_qubits=graph.num_nodes()
), constant
cost_hamiltonian, constant = build_max_cut_operator(graph)
print("Cost Function Hamiltonian:", cost_hamiltonian)
print("Constant:", constant)
Cost Function Hamiltonian: SparsePauliOp(['IIIZZ', 'IIZIZ', 'IIZZI', 'IZIZI', 'ZIZII', 'ZZIII'],
coeffs=[0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j])
Constant: -3.0
Hamiltoniano → circuito quantistico
L'Hamiltoniano contiene la definizione quantistica del problema. Ora è possibile creare un circuito quantistico che aiuterà a campionare buone soluzioni dal computer quantistico. Il QAOA è ispirato al quantum annealing e applica livelli alternati di operatori nel circuito quantistico.
L'idea generale è quella di partire dallo stato fondamentale di un sistema noto, sopra, e poi guidare il sistema verso lo stato fondamentale dell'operatore di costo di interesse. Questo viene fatto applicando gli operatori e con angoli