To't Hauptinholt springen

Quantum Approximate Optimization Algorithm

Bruukstied-Schatting: 22 Minuten op'n Heron r3 Prozessor (HENWIES: Dat is blots 'ne Schatting. Dien Looptied kann variern.)

Achtergrund

Dit Tutorial wiest, wo wi den Quantum Approximate Optimization Algorithm (QAOA) – 'ne hybride (quanten-klassische) iterative Methode – binnen den Kontext vun Qiskit-Patterns implementeern. Wi lööst eerst dat Maximum-Cut (oder Max-Cut) Problem för 'n lütten Graphen un leert denn, wo wi dat op Utility-Skala utföhren. All Hardware-Utföhrungen in dit Tutorial schöölt binnen de Tiedgrenz för den free tögänglichen Open Plan funkeern.

Dat Max-Cut-Problem is 'n Optimierungsproblem, wat swoor to lösen is (mehr seggt, dat is 'n NP-hard Problem) mit 'ne Reeg vun verscheden Anwennungen in Clustering, Netzwerkwetenschap un statistischer Physik. Dit Tutorial betrackt 'n Graphen vun Knoten, de dör Kanten verbunnen sünd, un will de Knoten in twee Mengen optelen, sodass de Tall vun de dör dissen Schnitt dörchreisd Kanten maximiert warrt.

Illustration of a max-cut problem

Vörutsettungen

Bevör wi mit dit Tutorial anfangen, seht to, dat ji dat Folgende installeert hebbt:

  • Qiskit SDK v1.0 oder later, mit Visualisierungs-Ünnerstütten
  • Qiskit Runtime v0.22 oder later (pip install qiskit-ibm-runtime)

Daarto bruukt ji Togäng to 'ne Instanz op de IBM Quantum Platform. Acht darop, dat dit Tutorial nich in'n Open Plan utföhrt warrn kann, wieldat dat Workloads mit Sessions utföhrt, de blots mit Premium Plan-Togäng verfögbor sünd.

Setup

import matplotlib
import matplotlib.pyplot as plt
import rustworkx as rx
from rustworkx.visualization import mpl_draw as draw_graph
import numpy as np
from scipy.optimize import minimize
from collections import defaultdict
from typing import Sequence

from qiskit.quantum_info import SparsePauliOp
from qiskit.circuit.library import QAOAAnsatz
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Session, EstimatorV2 as Estimator
from qiskit_ibm_runtime import SamplerV2 as Sampler

Deel I. QAOA in'n lütten Mootstav

De eerste Deel vun dit Tutorial bruukt 'n lütt Max-Cut-Problem, üm de Schritten för de Lösung vun 'n Optimierungsproblem mit 'n Quantencomputer to verdüütlichen.

Üm 'n beter Kontext to geven, bevör wi dit Problem op 'n Quantenalgorithmus afbilden, köönt ji beter verstahn, wo dat Max-Cut-Problem to 'n klassischen kombinatorischen Optimierungsproblem warrt, wenn wi eerst de Minimierung vun 'ne Funkschoon f(x)f(x) betrachten

minx{0,1}nf(x),\min_{x\in \{0, 1\}^n}f(x),

wobei de Ingav xx 'n Vektor is, wo sien Komponenten elkeen Knoten vun 'n Graphen entsprecht. Denn warrt jede vun disse Komponenten op entweder 00 oder 11 beschränkt (wat representeert, of se in'n Schnitt entholln sünd oder nich). Dissen lüttskalige Biespielfall bruukt 'n Graphen mit n=5n=5 Knoten.

Wi köönt 'ne Funkschoon vun 'n Knotenpaar i,ji,j schrieven, wat anwiest, of de entsprechend Kant (i,j)(i,j) in'n Schnitt liggt. To'n Biespeel is de Funkschoon xi+xj2xixjx_i + x_j - 2 x_i x_j nau denn 1, wenn entweder xix_i oder xjx_j gliek 1 is (wat bedüüdt, dat de Kant in'n Schnitt liggt) un süss null. Dat Problem, de Kanten in'n Schnitt to maximeren, kann formuleert warrn as

maxx{0,1}n(i,j)xi+xj2xixj,\max_{x\in \{0, 1\}^n} \sum_{(i,j)} x_i + x_j - 2 x_i x_j,

wat as Minimierung ümschreven warrn kann in de Form

minx{0,1}n(i,j)2xixjxixj.\min_{x\in \{0, 1\}^n} \sum_{(i,j)} 2 x_i x_j - x_i - x_j.

Dat Minimum vun f(x)f(x) in dissen Fall liggt vör, wenn de Tall vun de dör den Schnitt dörchreisd Kanten maximal is. As ji seht, hett dat noch nix mit Quantencomputing to doon. Wi mööt dit Problem in wat ümformuleren, wat 'n Quantencomputer verstahn kann. Initiali seert juu Problem, indem wi 'n Graphen mit n=5n=5 Knoten maakt.

n = 5

graph = rx.PyGraph()
graph.add_nodes_from(np.arange(0, n, 1))
edge_list = [
(0, 1, 1.0),
(0, 2, 1.0),
(0, 4, 1.0),
(1, 2, 1.0),
(2, 3, 1.0),
(3, 4, 1.0),
]
graph.add_edges_from(edge_list)
draw_graph(graph, node_size=600, with_labels=True)

Output of the previous code cell

Schritt 1: Klassische Ingav op 'n Quantenproblem afbilden

De eerste Schritt vun't Pattern besteiht darin, dat klassische Problem (Graph) op quantenmechanische Schaltkreise un Operatoren aftobilden. Daarto sünd dree Hövdschritten to ünnernahmen:

  1. Bruuk vun 'ne Reeg vun mathematische Ümformulierungen, üm dit Problem mit de Notation vun Quadratic Unconstrained Binary Optimization (QUBO) Problemen dartostellen.
  2. Ümformulierung vun't Optimierungsproblem as Hamilton-Operator, för den de Grundtostänn de Lösung entspricht, wo de Kostenfunkschoon minimieeert.
  3. Maaken vun 'n Quantenschaltkreis, de den Grundtostänn vun dissen Hamilton-Operator över 'n Prozess ähnlich dat Quantum Annealing vörbereit.

Acht darop: In de QAOA-Methodik wüllt wi letztlich 'n Operator (Hamilton-Operator) hemm, de de Kostenfunkschoon vun uns hybride Algorithmus darstellt, sowiet as 'n parametriseerten Schaltkreis (Ansatz), de Quantentostänne mit Kandidatenlösungen för dit Problem darstellt. Wi köönt ut disse Kandidatentostänne sampeln un se denn mit de Kostenfunkschoon bewerten.

Graph → Optimierungsproblem

De eerste Schritt vun de Afbildung is 'ne Notationsännerung. Dat Folgende drückt dit Problem in QUBO-Notation ut:

minx{0,1}nxTQx,\min_{x\in \{0, 1\}^n}x^T Q x,

wobei QQ 'ne n×nn\times n Matrix vun reelle Tallen is, nn de Tall vun de Knoten in juu Graphen entspricht, xx de boven inföhrde Vektor vun binäre Variable is un xTx^T de Transponeerde vun den Vektor xx bedüüdt.

Maximize
-2*x_0*x_1 - 2*x_0*x_2 - 2*x_0*x_4 - 2*x_1*x_2 - 2*x_2*x_3 - 2*x_3*x_4 + 3*x_0
+ 2*x_1 + 3*x_2 + 2*x_3 + 2*x_4

Subject to
No constraints

Binary variables (5)
x_0 x_1 x_2 x_3 x_4

Optimierungsproblem → Hamilton-Operator

Wi köönt denn dat QUBO-Problem as Hamilton-Operator ümformuleren (hier 'ne Matrix, wo de Energie vun 'n System darstellt):

HC=ijQijZiZj+ibiZi.H_C=\sum_{ij}Q_{ij}Z_iZ_j + \sum_i b_iZ_i.

Ümformulierungsschritten vun't QAOA-Problem to'n Hamilton-Operator

Üm to wiesen, wo dat QAOA-Problem op disse Wies ümschreven warrn kann, ersettt wi eerst de binären Variable xix_i dör 'n nijen Sett vun Variable zi{1,1}z_i\in\{-1, 1\} över

xi=1zi2.x_i = \frac{1-z_i}{2}.

Hier köönt ji sehn, dat wenn xix_i gliek 00 is, denn ziz_i gliek 11 sien mutt. Wenn de xix_i dör de ziz_i in't Optimierungsproblem (xTQxx^TQx) ersett warrt, kann 'ne äquivalente Formulierung kregen warrn.

xTQx=ijQijxixj=14ijQij(1zi)(1zj)=14ijQijzizj14ij(Qij+Qji)zi+n24.x^TQx=\sum_{ij}Q_{ij}x_ix_j \\ =\frac{1}{4}\sum_{ij}Q_{ij}(1-z_i)(1-z_j) \\=\frac{1}{4}\sum_{ij}Q_{ij}z_iz_j-\frac{1}{4}\sum_{ij}(Q_{ij}+Q_{ji})z_i + \frac{n^2}{4}.

Wenn wi nu bi=j(Qij+Qji)b_i=-\sum_{j}(Q_{ij}+Q_{ji}) definiern, den Vörfaktor wegnehmen un den konstanten Term n2n^2 weglaten, kriegen wi de beiden äquivalenten Formulierungen vun't sülve Optimierungsproblem.

minx{0,1}nxTQxminz{1,1}nzTQz+bTz\min_{x\in\{0,1\}^n} x^TQx\Longleftrightarrow \min_{z\in\{-1,1\}^n}z^TQz + b^Tz

Hier hängt bb vun QQ af. Acht darop, dat wi för de Erlangung vun zTQz+bTzz^TQz + b^Tz den Faktor 1/4 un 'n konstanten Offset vun n2n^2 weggelaten hebbt, de bi de Optimierung keen Roll speelt.

Üm nu 'ne Quantenformulierung vun't Problem to kriegen, erheven wi de Variable ziz_i to 'ne Pauli ZZ Matrix, liek as 'ne 2×22\times 2 Matrix vun de Form

Zi=(1001).Z_i = \begin{pmatrix}1 & 0 \\ 0 & -1\end{pmatrix}.

Wenn wi disse Matrizen in't boven Optimierungsproblem insetten, kriegen wi den folgenden Hamilton-Operator

HC=ijQijZiZj+ibiZi.H_C=\sum_{ij}Q_{ij}Z_iZ_j + \sum_i b_iZ_i.

Acht ok darop, dat de ZZ Matrizen in den Rekenruum vun den Quantencomputer inbedd sünd, dat heet in 'n Hilbert-Ruum vun de Gröte 2n×2n2^n\times 2^n. Dorüm schöölt wi Termen as ZiZjZ_iZ_j as dat Tensorprodukt ZiZjZ_i\otimes Z_j verstahn, wat in den 2n×2n2^n\times 2^n Hilbert-Ruum inbedd is. To'n Biespeel warrt in 'n Problem mit fief Entscheidungsvariabeln de Term Z1Z3Z_1Z_3 as IZ3IZ1II\otimes Z_3\otimes I\otimes Z_1\otimes I verstahn, wobei II de 2×22\times 2 Eenheitsmatrix is.

Dissen Hamilton-Operator warrt as Kostenfunkschoons-Hamilton-Operator beseekent. He hett de Egenschop, dat sien Grundtostänn de Lösung entspricht, wo de Kostenfunkschoon f(x)f(x) minimieeert. Üm juu Optimierungsproblem to lösen, mööt wi nu den Grundtostänn vun HCH_C (oder 'n Tostänn mit hoge Överlappung dormit) op'n Quantencomputer präpareren. Dat Sampeln ut dissen Tostänn warrt denn mit hoge Wahrschienlichkeit de Lösung to min f(x)min~f(x) levern. Betrach wi nu den Hamilton-Operator HCH_C för dat Max-Cut Problem. Elkeen Knoten vun den Graphen warrt 'n Qubit in'n Tostänn 0|0\rangle oder 1|1\rangle toordnet, wobei de Wert de Meng anwiest, to de de Knoten hört. Dat Ziel vun't Problem is dat, de Tall vun de Kanten (v1,v2)(v_1, v_2) to maximeren, för wo v1=0v_1 = |0\rangle un v2=1v_2 = |1\rangle gellt, oder ümkehrt. Wenn wi den ZZ Operator elkeen Qubit toordnen, wobei

Z0=0Z1=1 Z|0\rangle = |0\rangle \qquad Z|1\rangle = -|1\rangle

denn hört 'ne Kant (v1,v2)(v_1, v_2) to'n Schnitt, wenn de Eigenwert vun (Z1v1)(Z2v2)=1(Z_1|v_1\rangle) \cdot (Z_2|v_2\rangle) = -1 is; mit anner Wöör, de mit v1v_1 un v2v_2 assozieerten Qubits sünd verscheden. Ebenso hört (v1,v2)(v_1, v_2) nich to'n Schnitt, wenn de Eigenwert vun (Z1v1)(Z2v2)=1(Z_1|v_1\rangle) \cdot (Z_2|v_2\rangle) = 1 is. Acht darop, dat uns de genau Qubit-Tostänn, de elkeen Knoten toordnet is, nich interesseert, süss blots, of se över 'ne Kant henweg gliek sünd oder nich. Dat Max-Cut-Problem verlang vun uns, 'ne Toordnung vun de Qubits op de Knoten to finnen, sodass de Eigenwert vun den folgenden Hamilton-Operator minimiiert warrt

HC=(i,j)eQijZiZj. H_C = \sum_{(i,j) \in e} Q_{ij} \cdot Z_i Z_j.

Mit anner Wöör, bi=0b_i = 0 för all ii in't Max-Cut-Problem. De Wert vun QijQ_{ij} bedüüdt dat Gewicht vun de Kant. In dit Tutorial betrachten wi 'n ungewichteten Graphen, dat heet Qij=1.0Q_{ij} = 1.0 för all i,ji, j.

def build_max_cut_paulis(
graph: rx.PyGraph,
) -> list[tuple[str, list[int], float]]:
"""Convert the graph to Pauli list.

This function does the inverse of `build_max_cut_graph`
"""
pauli_list = []
for edge in list(graph.edge_list()):
weight = graph.get_edge_data(edge[0], edge[1])
pauli_list.append(("ZZ", [edge[0], edge[1]], weight))
return pauli_list

max_cut_paulis = build_max_cut_paulis(graph)
cost_hamiltonian = SparsePauliOp.from_sparse_list(max_cut_paulis, n)
print("Cost Function Hamiltonian:", cost_hamiltonian)
Cost Function Hamiltonian: SparsePauliOp(['IIIZZ', 'IIZIZ', 'ZIIIZ', 'IIZZI', 'IZZII', 'ZZIII'],
coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])

Hamilton-Operator → Quantenschaltkreis

De Hamilton-Operator HCH_C enthält de Quantendefinischoon vun juu Problem. Nu köönt wi 'n Quantenschaltkreis maken, de dorbi hülpt, gode Lösungen vun den Quantencomputer to sampeln. De QAOA is inspireert vun't Quantum Annealing un wendt afwesselnde Schichten vun Operatoren in'n Quantenschaltkreis an.

De allgemene Idee besteiht darin, in'n Grundtostänn vun 'n bekannten System to beginnen, Hn0H^{\otimes n}|0\rangle boven, un denn dat System in den Grundtostänn vun den Kostenoperator to stüern, an den wi interessiert sünd. Dat schütt dör Anwendung vun de Operatoren exp{iγkHC}\exp\{-i\gamma_k H_C\} un exp{iβkHm}\exp\{-i\beta_k H_m\} mit Winkeln γ1,...,γp\gamma_1,...,\gamma_p un β1,...,βp \beta_1,...,\beta_p~.

De vun uns genereert Quantenschaltkreis is parametriseert dör γi\gamma_i un βi\beta_i, sodass wi verscheden Werte vun γi\gamma_i un βi\beta_i utproberen un ut den resultierenden Tostänn sampeln köönt.

Circuit diagram with QAOA layers

In dissen Fall warrt wi 'n Biespeel mit 'ne QAOA-Schicht proberen, wo twee Parameter enthölt: γ1\gamma_1 un β1\beta_1.

circuit = QAOAAnsatz(cost_operator=cost_hamiltonian, reps=2)
circuit.measure_all()

circuit.draw("mpl")

Output of the previous code cell

circuit.parameters
ParameterView([ParameterVectorElement(β[0]), ParameterVectorElement(β[1]), ParameterVectorElement(γ[0]), ParameterVectorElement(γ[1])])

Schritt 2: Problem för Quanten-Hardware-Utföhrung optimeren

De boven Schaltkreis enthölt 'ne Reeg vun Abstraktionen, de nützlich sünd, üm över Quantenalgorithmen notodenken, aver op de Hardware nich utföhrbar sünd. Üm op 'ne QPU utföhrt warrn to könen, mutt de Schaltkreis 'ne Reeg vun Operationen dörlopen, wo den Transpilations- oder Schaltkreis-Optimierungs-Schritt vun't Pattern utmaakt.

De Qiskit-Bibliothek beed 'ne Reeg vun Transpilations-Pässen, wo 'ne breed Palette vun Schaltkreistransformationen afdeckt. Wi mööt sicherstellen, dat juu Schaltkreis för juu Tweck optimiiert is.

De Transpilation kann mehrere Schritten ümfaten, as to'n Biespeel:

  • Initialet Mapping vun de Qubits in'n Schaltkreis (as Entscheidungsvariabeln) op physische Qubits op't Gerät.
  • Unrolling vun de Anwiesungen in'n Quantenschaltkreis to de hardware-native Anwiesungen, wo dat Backend versteih.
  • Routing vun beliebige Qubits in'n Schaltkreis, de interageern, to physische Qubits, de benachbart toenanner sünd.
  • Fehlerünnerdrückung dör Hintofögen vun Enkelqubit-Gates för Ruschünnerdrückung mit dynamische Entkopplung.

Mehr Informatschonen to de Transpilation finnt ji in uns Dokumentation.

De folgende Code transformiert un optimiiert den abstrakten Schaltkreis in 'n Format, wat för de Utföhrung op een vun de över de Cloud tögänglichen Geräten mit den Qiskit IBM Runtime Service klaar is.

service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
print(backend)

# Create pass manager for transpilation
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)

candidate_circuit = pm.run(circuit)
candidate_circuit.draw("mpl", fold=False, idle_wires=False)
<IBMBackend('test_heron_pok_1')>

Output of the previous code cell

Schritt 3: Utföhrung mit Qiskit Primitives

In'n QAOA-Workflow warrt de optimalen QAOA-Parameter in 'ne iterative Optimierungsslöp funnen, wo 'ne Reeg vun Schaltkreisbewertungen utföhrt un 'n klassischen Optimierer bruukt, üm de optimalen βk\beta_k un γk\gamma_k Parameter to finnen. Disse Utföhrungsslöp warrt över de folgenden Schritten utföhrt:

  1. Defineren vun de initialen Parameter
  2. Instanziierung vun 'ne nije Session, wo de Optimierungsslöp un dat Primitive enthölt, wat to'n Sampeln vun den Schaltkreis bruukt warrt
  3. Sobald 'n optimalen Parametersett funnen is, föhrt wi den Schaltkreis 'n letzt Mal ut, üm 'ne finale Verdelung to kriegen, wo in'n Post-Processing-Schritt bruukt warrt.

Schaltkreis mit initialen Parametern defineren

Wi beginnt mit willkürlich wählten Parametern.

initial_gamma = np.pi
initial_beta = np.pi / 2
init_params = [initial_beta, initial_beta, initial_gamma, initial_gamma]

Backend un Utföhrungs-Primitive defineren

Bruuk de Qiskit Runtime Primitives, üm mit IBM® Backends to interageren. De beiden Primitives sünd Sampler un Estimator, un de Wahl vun't Primitive hängt dorvan af, welk Oort vun Meeten wi op'n Quantencomputer utföhren wüllt. För de Minimierung vun HCH_C bruukt wi den Estimator, dat de Meeten vun de Kostenfunkschoon eenfach de Verwachtungswert vun HC\langle H_C \rangle is.

Utföhren

De Primitives beedt 'ne Vielheit vun Utföhrungsmodi to de Planung vun Workloads op Quantengeräten, un 'n QAOA-Workflow löppt iterativ in 'ne Session.

Illustration showing the behavior of Single job, Batch, and Session runtime modes.

Wi köönt de sampler-baseerte Kostenfunkschoon in de SciPy-Minimierungsroutine insteken, üm de optimalen Parameter to finnen.

def cost_func_estimator(params, ansatz, hamiltonian, estimator):
# transform the observable defined on virtual qubits to
# an observable defined on all physical qubits
isa_hamiltonian = hamiltonian.apply_layout(ansatz.layout)

pub = (ansatz, isa_hamiltonian, params)
job = estimator.run([pub])

results = job.result()[0]
cost = results.data.evs

objective_func_vals.append(cost)

return cost
objective_func_vals = []  # Global variable
with Session(backend=backend) as session:
# If using qiskit-ibm-runtime<0.24.0, change `mode=` to `session=`
estimator = Estimator(mode=session)
estimator.options.default_shots = 1000

# Set simple error suppression/mitigation options
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XY4"
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"

result = minimize(
cost_func_estimator,
init_params,
args=(candidate_circuit, cost_hamiltonian, estimator),
method="COBYLA",
tol=1e-2,
)
print(result)
message: Return from COBYLA because the trust region radius reaches its lower bound.
success: True
status: 0
fun: -1.6295230263157894
x: [ 1.530e+00 1.439e+00 4.071e+00 4.434e+00]
nfev: 26
maxcv: 0.0

De Optimierer kunn de Kosten reduzeern un beter Parameter för den Schaltkreis finnen.

plt.figure(figsize=(12, 6))
plt.plot(objective_func_vals)
plt.xlabel("Iteration")
plt.ylabel("Cost")
plt.show()

Output of the previous code cell

Sobald wi de optimalen Parameter för den Schaltkreis funnen hebbt, köönt wi disse Parameter towies un de mit de optimierten Parametern erhollen finale Verdelung sampeln. Hier schull dat Sampler Primitive bruukt warrn, dat dat de Wahrschienlichkeitsverdelung vun Bitstring-Metungen is, wo den optimalen Schnitt vun den Graphen entspricht.

Acht darop: Dat bedüüdt, 'n Quantentostänn ψ\psi in'n Computer to präpareren un em denn to meten. 'ne Meeten warrt den Tostänn in 'n enkel ten Berekenungsbasistostänn kollab eren laten - to'n Biespeel 010101110000... - de 'ne Kandidatenlösung xx för uns urs prüngliche Optimierungsproblem (maxf(x)\max f(x) oder minf(x)\min f(x) je na Opgav) entspricht.

optimized_circuit = candidate_circuit.assign_parameters(result.x)
optimized_circuit.draw("mpl", fold=False, idle_wires=False)

Output of the previous code cell

# If using qiskit-ibm-runtime<0.24.0, change `mode=` to `backend=`
sampler = Sampler(mode=backend)
sampler.options.default_shots = 10000

# Set simple error suppression/mitigation options
sampler.options.dynamical_decoupling.enable = True
sampler.options.dynamical_decoupling.sequence_type = "XY4"
sampler.options.twirling.enable_gates = True
sampler.options.twirling.num_randomizations = "auto"

pub = (optimized_circuit,)
job = sampler.run([pub], shots=int(1e4))
counts_int = job.result()[0].data.meas.get_int_counts()
counts_bin = job.result()[0].data.meas.get_counts()
shots = sum(counts_int.values())
final_distribution_int = {key: val / shots for key, val in counts_int.items()}
final_distribution_bin = {key: val / shots for key, val in counts_bin.items()}
print(final_distribution_int)
{28: 0.0328, 11: 0.0343, 2: 0.0296, 25: 0.0308, 16: 0.0303, 27: 0.0302, 13: 0.0323, 7: 0.0312, 4: 0.0296, 9: 0.0295, 26: 0.0321, 30: 0.031, 23: 0.0324, 31: 0.0303, 21: 0.0335, 15: 0.0317, 12: 0.0309, 29: 0.0297, 3: 0.0313, 5: 0.0312, 6: 0.0274, 10: 0.0329, 22: 0.0353, 0: 0.0315, 20: 0.0326, 8: 0.0322, 14: 0.0306, 17: 0.0295, 18: 0.0279, 1: 0.0325, 24: 0.0334, 19: 0.0295}

Schritt 4: Nabearbeiten un Törüchgeven vun't Resultat in't wünschte klassische Format

De Nabearbetensch ritt interpre teert de Sampling-Utgav, üm 'ne Lösung för juu ursprüngliche Problem törüchtogeven. In dissen Fall sünd wi an den Bitstring mit de högste Wahrschienlichkeit interessiert, dat de den optimalen Schnitt bestimmt. De Symmetrien in't Problem erlöövt veer mögliche Lösungen, un de Sampling-Prozess warrt een dorvun mit 'ne etwat högere Wahrschienlichkeit törüchgeven, aver ji köönt in de ünner darstellte Verdelung sehn, dat veer vun de Bitstrings düütlich wahrschienlicher sünd as de Rest.

# auxiliary functions to sample most likely bitstring
def to_bitstring(integer, num_bits):
result = np.binary_repr(integer, width=num_bits)
return [int(digit) for digit in result]

keys = list(final_distribution_int.keys())
values = list(final_distribution_int.values())
most_likely = keys[np.argmax(np.abs(values))]
most_likely_bitstring = to_bitstring(most_likely, len(graph))
most_likely_bitstring.reverse()

print("Result bitstring:", most_likely_bitstring)
Result bitstring: [0, 1, 1, 0, 1]
matplotlib.rcParams.update({"font.size": 10})
final_bits = final_distribution_bin
values = np.abs(list(final_bits.values()))
top_4_values = sorted(values, reverse=True)[:4]
positions = []
for value in top_4_values:
positions.append(np.where(values == value)[0])
fig = plt.figure(figsize=(11, 6))
ax = fig.add_subplot(1, 1, 1)
plt.xticks(rotation=45)
plt.title("Result Distribution")
plt.xlabel("Bitstrings (reversed)")
plt.ylabel("Probability")
ax.bar(list(final_bits.keys()), list(final_bits.values()), color="tab:grey")
for p in positions:
ax.get_children()[int(p[0])].set_color("tab:purple")
plt.show()

Output of the previous code cell

Besten Schnitt visualiseren

Ut den optimalen Bit-String köönt wi denn dissen Schnitt op'n originalen Graphen visualiseren.

# auxiliary function to plot graphs
def plot_result(G, x):
colors = ["tab:grey" if i == 0 else "tab:purple" for i in x]
pos, _default_axes = rx.spring_layout(G), plt.axes(frameon=True)
rx.visualization.mpl_draw(
G, node_color=colors, node_size=100, alpha=0.8, pos=pos
)

plot_result(graph, most_likely_bitstring)

Output of the previous code cell

Un bereken den Wert vun den Schnitt:

def evaluate_sample(x: Sequence[int], graph: rx.PyGraph) -> float:
assert len(x) == len(
list(graph.nodes())
), "The length of x must coincide with the number of nodes in the graph."
return sum(
x[u] * (1 - x[v]) + x[v] * (1 - x[u])
for u, v in list(graph.edge_list())
)

cut_value = evaluate_sample(most_likely_bitstring, graph)
print("The value of the cut is:", cut_value)
The value of the cut is: 5

Deel II. Grötter maken!

Wi hebbt Togäng to veel Geräden mit över 100 Qubits op de IBM Quantum® Platform. Wähl een ut, op den wi Max-Cut op 'n 100-Knoten gewichteten Graphen löst. Dat is 'n "utility-scale" Problem. De Schritten för de Workflow to boen warrt genau so befögt as boven, aver mit 'n veel grötteren Graphen.

n = 100  # Number of nodes in graph
graph_100 = rx.PyGraph()
graph_100.add_nodes_from(np.arange(0, n, 1))
elist = []
for edge in backend.coupling_map:
if edge[0] < n and edge[1] < n:
elist.append((edge[0], edge[1], 1.0))
graph_100.add_edges_from(elist)
draw_graph(graph_100, node_size=200, with_labels=True, width=1)

Output of the previous code cell

Schritt 1: Klassische Ingaven op 'n Quantenproblem afbilden

Graph → Hamilton-Operator

Eerst konvertern wi den Graphen, den wi lösen wüllt, direkt in 'n Hamilton-Operator, de för QAOA geegnet is.

max_cut_paulis_100 = build_max_cut_paulis(graph_100)

cost_hamiltonian_100 = SparsePauliOp.from_sparse_list(max_cut_paulis_100, 100)
print("Cost Function Hamiltonian:", cost_hamiltonian_100)
Cost Function Hamiltonian: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ', ...],
coeffs=[1.+0.j, 1.+0.j, ...])

Hamilton-Operator → Quantenschaltkreis

circuit_100 = QAOAAnsatz(cost_operator=cost_hamiltonian_100, reps=1)
circuit_100.measure_all()

circuit_100.draw("mpl", fold=False, scale=0.2, idle_wires=False)

Output of the previous code cell

Schritt 2: Problem för Quantenutföhrung optimeren

Üm den Schaltkreis-Optimierungsschritt för utility-scale Problemen to skaleren, köönt wi de hoge Performance-Transpilationsstrategien nutzen, wo in't Qiskit SDK v1.0 inföhrt worrn. Anner Warktüüch sünd de nije Transpiler-Service mit AI-verstärkde Transpiler-Pässen.

pm = generate_preset_pass_manager(optimization_level=3, backend=backend)

candidate_circuit_100 = pm.run(circuit_100)
candidate_circuit_100.draw("mpl", fold=False, scale=0.1, idle_wires=False)

Output of the previous code cell

Schritt 3: Utföhrung mit Qiskit Primitives

Üm QAOA to lopen, mööt wi de optimalen Parameter γk\gamma_k un βk\beta_k weten, wo wi in den variational Schaltkreis insetten. Optimert disse Parameter, indem wi 'ne Optimierungsslöp op't Gerät lopen laat. De Zell schickt Jobs af, bet de Wert vun de Kostenfunkschoon konvergiiert is un de optimalen Parameter för γk\gamma_k un βk\beta_k bestimmt sünd.

Kandidatenlösung finnen dör Lopen vun de Optimierung op't Gerät

Eerst lopen wi de Optimierungsslöp för de Schaltkreisparameter op't Gerät.

initial_gamma = np.pi
initial_beta = np.pi / 2
init_params = [initial_beta, initial_gamma]

objective_func_vals = [] # Global variable
with Session(backend=backend) as session:
estimator = Estimator(mode=session)
estimator.options.default_shots = 1000
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XY4"
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"

result = minimize(
cost_func_estimator,
init_params,
args=(candidate_circuit_100, cost_hamiltonian_100, estimator),
method="COBYLA",
)
print(result)
message: Return from COBYLA because the trust region radius reaches its lower bound.
success: True
status: 0
fun: -3.9939191365979383
x: [ 1.571e+00 3.142e+00]
nfev: 29
maxcv: 0.0

Sobald de optimalen Parameter ut't Lopen vun QAOA op't Gerät funnen sünd, wiest wi disse Parameter den Schaltkreis to.

optimized_circuit_100 = candidate_circuit_100.assign_parameters(result.x)
optimized_circuit_100.draw("mpl", fold=False, idle_wires=False)

Output of the previous code cell

Letztlich föhrt wi den Schaltkreis mit de optimalen Parameter ut, üm ut de entsprechend Verdelung to sampeln.

sampler = Sampler(mode=backend)
sampler.options.default_shots = 10000
sampler.options.dynamical_decoupling.enable = True
sampler.options.dynamical_decoupling.sequence_type = "XY4"
sampler.options.twirling.enable_gates = True
sampler.options.twirling.num_randomizations = "auto"

pub = (optimized_circuit_100,)
job = sampler.run([pub], shots=int(1e4))

counts_int = job.result()[0].data.meas.get_int_counts()
counts_bin = job.result()[0].data.meas.get_counts()
shots = sum(counts_int.values())
final_distribution_100_int = {
key: val / shots for key, val in counts_int.items()
}

Pröövt, dat de Kosten, wo in de Optimierungsslöp minimiiert worrn, to 'n bestimmten Wert konvergiiert sünd.

plt.figure(figsize=(12, 6))
plt.plot(objective_func_vals)
plt.xlabel("Iteration")
plt.ylabel("Cost")
plt.show()

Output of the previous code cell

Schritt 4: Nabearbeiten un Törüchgeven vun't Resultat in't wünschte klassische Format

Geven, dat de Wahrschienlichkeit vun elkeen Lösung neddrig is, extraheren wi de Lösung, wo de neddrigsten Kosten entspricht.

_PARITY = np.array(
[-1 if bin(i).count("1") % 2 else 1 for i in range(256)],
dtype=np.complex128,
)

def evaluate_sparse_pauli(state: int, observable: SparsePauliOp) -> complex:
packed_uint8 = np.packbits(observable.paulis.z, axis=1, bitorder="little")
state_bytes = np.frombuffer(
state.to_bytes(packed_uint8.shape[1], "little"), dtype=np.uint8
)
reduced = np.bitwise_xor.reduce(packed_uint8 & state_bytes, axis=1)
return np.sum(observable.coeffs * _PARITY[reduced])

def best_solution(samples, hamiltonian):
min_cost = 1000
min_sol = None
for bit_str in samples.keys():
candidate_sol = int(bit_str)
fval = evaluate_sparse_pauli(candidate_sol, hamiltonian).real
if fval <= min_cost:
min_sol = candidate_sol
return min_sol

best_sol_100 = best_solution(final_distribution_100_int, cost_hamiltonian_100)
best_sol_bitstring_100 = to_bitstring(int(best_sol_100), len(graph_100))
best_sol_bitstring_100.reverse()

print("Result bitstring:", best_sol_bitstring_100)
Result bitstring: [1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1]

As nächste visualiseren wi den Schnitt. Knoten mit de sülve Farv hört to de sülve Grupp.

plot_result(graph_100, best_sol_bitstring_100)

Output of the previous code cell

Bereken den Wert vun den Schnitt.

cut_value_100 = evaluate_sample(best_sol_bitstring_100, graph_100)
print("The value of the cut is:", cut_value_100)
The value of the cut is: 124

Nu mööt wi den Zielfunktionswert vun elkeen Sample bereken, dat wi op'n Quantencomputer meten hebbt. Dat Sample mit den neddrigsten Zielfunktionswert is de Lösung, wo vun den Quantencomputer törüchgeven warrt.

def _plot_cdf(objective_values: dict, ax, color):
x_vals = sorted(objective_values.keys(), reverse=True)
y_vals = np.cumsum([objective_values[x] for x in x_vals])
ax.plot(x_vals, y_vals, color=color)

def plot_cdf(dist, ax, title):
_plot_cdf(dist, ax, "C1")
ax.vlines(min(list(dist.keys())), 0, 1, "C1", linestyle="--")
ax.set_title(title)
ax.set_xlabel("Objective function value")
ax.set_ylabel("Cumulative distribution function")
ax.grid(alpha=0.3)

def samples_to_objective_values(samples, hamiltonian):
objective_values = defaultdict(float)
for bit_str, prob in samples.items():
candidate_sol = int(bit_str)
fval = evaluate_sparse_pauli(candidate_sol, hamiltonian).real
objective_values[fval] += prob
return objective_values

result_dist = samples_to_objective_values(
final_distribution_100_int, cost_hamiltonian_100
)

Letztlich köönt wi de kumulative Verdelingsfunkschoon plotten, üm to visualiseren, wo elkeen Sample to de totale Wahrschienlichkeitsverdelung un den entsprechenden Zielfunktionswert bidreggt. De horizontal Utdehnung wiest den Bereich vun Zielfunktionswerte vun de Samples in de finale Verdelung. Ideal Wies schöölt wi sehn, dat de kumulative Verdelingsfunkschoon "Sprünge" an't ünnere Enn vun de Zielfunktionswertass hett. Dat schull bedüden, dat wenig Lösungen mit neddrige Kosten 'ne hoge Wahrschienlichkeit hebbt, sampelt to warrn. 'ne glatte, breed Kurv wiest an, dat elkeen Sample ähnlich wahrschienlich is un se veel verscheden Zielfunktionswerte hemm köönt, neddrig oder hoog.

fig, ax = plt.subplots(1, 1, figsize=(8, 6))
plot_cdf(result_dist, ax, "Eagle device")

Output of the previous code cell

Sluss

Dit Tutorial hett wiest, wo wi 'n Optimierungsproblem mit 'n Quantencomputer löst mit dat Qiskit Patterns Framework. De Demonstration enthölt 'n utility-scale Biespeel, mit Schaltkreisgröten, de nich genau klassisch simuleert warrn köönt. Momentan övverdoot Quantencomputer nich de klassischen Computer för kombinatorische Optimierung wegen Rusch. Aver de Hardware verbetert stetig, un nije Algorithmen för Quantencomputer warrt ständig ent wickelt. Tatsächlich warrt veel vun de Forschung, de an Quanten-Heuristiken för kombinatorische Optimierung arbeidt, mit klassische Simulationen testet, de blots 'ne lütte Tall vun Qubits erlöövt, typisch üm de 20 Qubits. Nu, mit grötteren Qubit-Tallen un Geräden mit weniger Rusch, warrt Forschers doran arbeiden könen, disse Quanten-Heuristiken bi groten Problemgröten op Quanten-Hardware to benchmarken.

Tutorial-Ümmfraag

Nimm doran deel an disse korte Ümmfraag, üm Feedback to dit Tutorial to geven. Juu Insichten warrt uns helpen, uns Inholtsanbod un Brukerfarung to verbeten.

Link to de Ümmfraag