Skip to main content
IBM Quantum Platform

QAOA mit Warm-Start unter Verwendung des Qiskit-Add-ons „Optimization Mapper“

Geschätzte Nutzungsdauer: 9 Minuten auf einem Heron r3 (HINWEIS: Dies ist lediglich eine Schätzung. (Die Laufzeit kann variieren.)


Lernziele

  • Wie lässt sich ein Max-Cut-Problem mithilfe von auf eine QUBO-Formulierung (Quadratic Unconstrained Binary Optimization) abbilden? qiskit-addon-opt-mapper
  • So implementieren und führen Sie Standard-QAOA auf einem Simulator aus
  • Anwendung von WS-QAOA durch Berechnung der Relaxation des quadratischen Programms (QP) und Aufbau der Warm-Start-Schaltung
  • Wie lassen sich die Energiekonvergenz und die Lösungsqualität zwischen Standard-QAOA und WS-QAOA vergleichen?

Voraussetzungen


Hintergrund

Der Quantum Approximate Optimization Algorithm (QAOA) ist ein hybrider quanten-klassischer Algorithmus, der zur Lösung kombinatorischer Optimierungsprobleme wie Max-Cut und allgemeiner QUBO-Formulierungen entwickelt wurde. Eine grundlegende Einführung in QAOA mit Qiskit finden Sie im QAOA-Tutorial; fortgeschrittene Techniken zum Erstellen von Schaltkreisen werden im QAOA-Tutorial für Fortgeschrittene behandelt.

Im Standard-QAOA:

  • Der Anfangszustand ist die gleichmäßige Überlagerung ∣+⟩⊗n|+\rangle^{\otimes n}.
  • Die Variationsparameter werden zufällig initialisiert.
  • Ein klassischer Optimierer sucht nach Parametern, die die Kostenfunktion minimieren.

Bei realistischen Problemgrößen und rauschbehafteter Quantenhardware kann eine zufällige Initialisierung jedoch zu einer langsamen Konvergenz, ungünstigen lokalen Minima und erhöhten Optimierungsaufwänden führen.

Warm-Start-QAOA (WS-QAOA) verbessert dies, indem klassische Optimierungserkenntnisse direkt in den Quantenschaltkreis integriert werden. Dieses Tutorial orientiert sich an den von Egger, Mareček und Woerner in „Warm-starting quantum optimization“ vorgestellten Methoden. Der Kerngedanke lautet:

  1. Lösen Sie eine kontinuierliche Relaxation des ursprünglichen binären Problems (ein quadratisches Programm über [0,1]n[0,1]^n anstelle von {0,1}n\{0,1\}^n ).
  2. Kodieren Sie die entspannte Lösung ci∗∈[0,1]c^*_i \in [0,1] in einen benutzerdefinierten Anfangszustand, indem Sie YY -Rotationswinkel θi=2arcsin⁡(ci∗)\theta_i = 2\arcsin(\sqrt{c^*_i}) verwenden, sodass das Qubit ii in einem Zustand beginnt, dessen Wahrscheinlichkeit, ∣1⟩|1\rangle zu messen, ci∗c^*_i beträgt.
  3. Ersetzen Sie den Standard- XX -Mixer durch einen benutzerdefinierten Mixer, dessen Grundzustand der Warm-Start-Anfangszustand ist, um sicherzustellen, dass der Algorithmus nahe der klassischen Lösung startet und die Umgebung erkunden kann.

Ein Regularisierungsparameter ε∈[0,0.5]\varepsilon \in [0, 0.5] begrenzt ci∗c^*_i außerhalb des Bereichs von 0 und 1, um Erreichbarkeitsprobleme zu vermeiden; Qubits, die in ∣0⟩|0\rangle oder ∣1⟩|1\rangle initialisiert wurden, können durch den Kosten-Hamilton-Operator nicht verschoben werden. Bei ε=0.5\varepsilon = 0.5 lässt sich WS-QAOA genau auf Standard-QAOA reduzieren.

Zur Modellierung des Problems wird das qiskit-addon-opt-mapper Paket verwendet, dessen Maxcut Anwendungsklasse das QUBO direkt aus einem Graphen aufbaut und dessen Konverter und Übersetzer das resultierende Problem auf Quanten-Hamilton-Operatoren abbilden.


Anforderungen

Bevor Sie mit dieser Anleitung beginnen, stellen Sie bitte sicher, dass Folgendes installiert ist:

  • Qiskit SDK v2.0 oder höher, mit Visual isierungsunterstützung
  • Qiskit Runtime v0.43 oder später (pip install qiskit-ibm-runtime)
  • Optimization Mapper – Qiskit-Add-on (pip install qiskit-addon-opt-mapper)
  • SciPy (pip install scipy)
  • NetworkX (pip install networkx)

Einrichtung

Importieren Sie alle erforderlichen Bibliotheken und definieren Sie die Hilfsfunktionen, die in diesem Tutorial verwendet werden.

import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
from scipy.optimize import minimize

from qiskit.circuit import QuantumCircuit, ParameterVector
from qiskit.circuit.library import qaoa_ansatz
from qiskit.quantum_info import Statevector
from qiskit.primitives import StatevectorEstimator, StatevectorSampler
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import (
    QiskitRuntimeService,
    Session,
    EstimatorOptions,
    EstimatorV2 as Estimator,
    SamplerV2 as Sampler,
)

from qiskit_addon_opt_mapper.applications import Maxcut
from qiskit_addon_opt_mapper.converters import OptimizationProblemToQubo
from qiskit_addon_opt_mapper.translators import to_ising

Beispiel für einen Simulator im kleinen Maßstab

Als Beispiel dienen wir uns ein kleines Max-Cut -Problem in einem gewichteten Graphen an. Max-cut fragt: Gegeben sei ein Graph G=(V,E)G=(V,E) mit Kantengewichten wijw_{ij}. Finde eine Partition der Knoten in zwei Mengen SS und Sˉ\bar{S}, die das Gesamtgewicht der Kanten, die den Schnittpunkt überqueren, maximiert.

Als QUBO-Minimierungsproblem lässt sich das Max-Cut-Problem wie folgt darstellen: min⁡x∈{0,1}n−∑(i,j)∈Ewij(xi+xj−2xixj)\min_{x \in \{0,1\}^n} -\sum_{(i,j) \in E} w_{ij}(x_i + x_j - 2x_i x_j)

Aus Gründen der Handhabbarkeit im Simulator arbeiten wir mit einem Graphen mit vier Knoten.

Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden

Wir definieren das Max-Cut-Problem mithilfe der Maxcut Anwendungsklasse aus qiskit-addon-opt-mapper, die die QUBO-Formulierung direkt aus einem Graphen erstellt. Anschließend wandeln wir es in ein QUBO um und übertragen es auf einen für QAOA geeigneten Ising-Hamiltonian (SparsePauliOp). Außerdem lösen wir die kontinuierliche Relaxation des QUBO – wobei wir die binäre Nebenbedingung xi∈{0,1}x_i \in \{0,1\} durch xi∈[0,1]x_i \in [0,1] ersetzen –, um den Warm-Start-Ausgangspunkt c∗c^* zu erhalten.

# Define a 4-node weighted graph for the max-cut problem
n_nodes = 4
edges = [(0, 1, 1.0), (0, 2, 1.0), (1, 2, 1.0), (1, 3, 1.0), (2, 3, 1.0)]

G = nx.Graph()
G.add_nodes_from(range(n_nodes))
G.add_weighted_edges_from(edges)

pos = nx.spring_layout(G, seed=42)
edge_labels = {(u, v): d["weight"] for u, v, d in G.edges(data=True)}

fig, ax = plt.subplots(figsize=(4, 3))
nx.draw(G, pos, with_labels=True, node_color="lightblue", ax=ax)
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, ax=ax)
ax.set_title("Max-Cut graph")
plt.tight_layout()
plt.show()

Output:

Output of the previous code cell

Der Graph hat fünf Kanten. Die optimale Max-Cut-Partitionierung unterteilt die Knoten in S={0,3}S = \{0, 3\} und Sˉ={1,2}\bar{S} = \{1, 2\} (bzw. deren Komplement) und trennt dabei vier der fünf Kanten, was einen Schnittwert von 4 ergibt.

# Build the max-cut problem directly from the NetworkX graph using the
# Maxcut application class. Internally it constructs the QUBO
#   minimize  -sum_{(i,j) in E} w_ij * (x_i + x_j - 2*x_i*x_j)
# (each edge contributes -w to the linear terms and +2w to the quadratic
# term), so we get the same OptimizationProblem without the boilerplate.
maxcut = Maxcut(G)
prob = maxcut.to_optimization_problem()
print(prob.prettyprint())

Output:

Problem name: Max-cut

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

Subject to
  No constraints

  Binary variables (4)
    x_0 x_1 x_2 x_3

Die Maxcut Klasse umschließt die QUBO-Konstruktion, sodass wir das Max-Cut-Ziel nicht manuell erweitern müssen. Die ausgedruckte Zielfunktion zeigt den linearen Koeffizienten jeder Variablen (ihren individuellen Beitrag zur Teilung) sowie den quadratischen Koeffizienten jedes Kreuzterms (die Strafe dafür, zwei benachbarte Knoten auf dieselbe Seite zu setzen). Der von to_optimization_problem() zurückgegebene Untertyp OptimizationProblem unterstützt Binär-, Ganzzahl-, Kontinuums- und Spin-Variablen und entspricht genau dem Objekt, das von den im nächsten Schritt verwendeten Konvertern und Übersetzern erwartet wird.

# Convert the OptimizationProblem to a QUBO, then translate to an Ising Hamiltonian
#
# The substitution x_i = (1 - z_i)/2  maps binary variables to spin operators,
# yielding a Hamiltonian H_C = sum_i h_i Z_i + sum_{i<j} J_ij Z_i Z_j + constant.
# QAOA minimizes <H_C> to find the ground state, which encodes the optimal cut.
converter = OptimizationProblemToQubo()
qubo = converter.convert(prob)

cost_operator, offset = to_ising(qubo)
n_qubits = cost_operator.num_qubits

print(f"Cost Hamiltonian H_C ({n_qubits} qubits):")
print(cost_operator)
print(f"\nOffset (constant shift): {offset}")
print("  QUBO value = Ising energy + offset")

Output:

Cost Hamiltonian H_C (4 qubits):
SparsePauliOp(['IIZZ', 'IZIZ', 'IZZI', 'ZIZI', 'ZZII'],
              coeffs=[0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j])

Offset (constant shift): -2.5
  QUBO value = Ising energy + offset

Der to_ising Translator gibt ein SparsePauliOp HCH_C und einen Skalar offset zurück, sodass QUBO value=⟨HC⟩+offset\text{QUBO value} = \langle H_C \rangle + \text{offset} gilt. Für dieses Max-Cut-Problem mit ausschließlich Einheitsgewichten gilt hi=0h_i = 0 für alle Qubits (der Graph ist nach der Substitution xi→zix_i \to z_i linear symmetrisch), und jede Kante trägt eine Kopplung ZiZjZ_i Z_j mit der Stärke +0.5+0.5 bei. Der minimale Eigenwert von HCH_C entspricht dem maximalen Schnitt.

# Solve the continuous (QP) relaxation to obtain the warm-start point c*
#
# The QP relaxation replaces the binary constraint x_i in {0,1} with x_i in [0,1]
# and minimizes the same quadratic objective. Its solution c*_i gives the
# probability that variable i should be 1 according to the classical relaxation.
#
# The max-cut QUBO has a non-convex quadratic matrix (negative eigenvalues),
# so the relaxed problem has multiple local minima. A naive single start from
# [0.5,...,0.5] converges to the symmetric saddle point c* = [0.5,...,0.5],
# which carries no useful structural information about the problem.
# Multi-start optimization is used to reliably find the global minimum.
Q = qubo.objective.quadratic.to_array(symmetric=True)
mu = qubo.objective.linear.to_array()


def qp_objective(x_cont):
    """Continuous relaxation of the QUBO objective."""
    return x_cont @ Q @ x_cont + mu @ x_cont + qubo.objective.constant


bounds = [(0.0, 1.0)] * n_qubits

rng = np.random.default_rng(42)
best_val = np.inf
c_star = None
for _ in range(200):
    x0 = rng.uniform(0.0, 1.0, n_qubits)
    result = minimize(qp_objective, x0, method="L-BFGS-B", bounds=bounds)
    if result.fun < best_val:
        best_val = result.fun
        c_star = result.x

print(f"QP relaxation solution c* = {np.round(c_star, 4)}")
print(f"QP objective value        = {best_val:.4f}")

Output:

QP relaxation solution c* = [1. 0. 0. 1.]
QP objective value        = -4.0000

Der Multi-Start-Solver findet c∗=[1,0,0,1]c^* = [1, 0, 0, 1] (bzw. dessen Komplement [0,1,1,0][0, 1, 1, 0] ), was die tatsächlich optimale binäre Lösung darstellt. Bei diesem Problem ist die QP-Relaxation eng, das kontinuierliche Minimum fällt mit dem ganzzahligen Optimum zusammen, was bedeutet, dass die Relaxation sofort den besten Schnitt identifiziert. Nach der Regularisierung mit „ ε=0.25\varepsilon = 0.25 “ in Schritt 2 wird diese Lösung in den Warm-Start-Ausgangszustand kodiert.

Schritt 2: Optimierung des Problems für die Ausführung auf Quantenhardware

Wir bauen zwei QAOA-Schaltungen auf und ermitteln die Warm-Start-Winkel anhand der QP-Lösung.

Bei der Standard-QAOA wird die gleichförmige Überlagerung ∣+⟩⊗n|+\rangle^{\otimes n} als Anfangszustand verwendet, sowie der Standard- XX -Mixer HM=−∑iXiH_M = -\sum_i X_i, der als ∏iRX(−2β)\prod_i R_X(-2\beta) pro Schicht implementiert ist.

Das Warm-Start-QAOA-Verfahren (WS-QAOA) aus [1] nimmt pro Qubit zwei strukturelle Änderungen vor ii :

  • Ausgangszustand: RY(θi)∣0⟩R_Y(\theta_i)|0\rangle mit θi=2arcsin⁡(ci∗)\theta_i = 2\arcsin(\sqrt{c^*_i}), sodass die Wahrscheinlichkeit, ∣1⟩|1\rangle zu messen, gleich ci∗c^*_i ist.
  • Benutzerdefinierter Mixer: „RY(θi) RZ(−2β) RY(−θi)R_Y(\theta_i)\, R_Z(-2\beta)\, R_Y(-\theta_i) “, dessen Grundzustand „ RY(θi)∣0⟩R_Y(\theta_i)|0\rangle “ ist. Das bedeutet, dass WS-QAOA im Grundzustand seines eigenen Mischers beginnt – dieselbe Eigenschaft, die auch die Standard-QAOA mit dem „ ∣+⟩|+\rangle “- und dem „ XX “-Mischer erfüllt.

Anmerkung zu den Schichten: Bei p=1(einer einzelnen QAOA-Schicht) ist das Standard-QAOA-Verfahren analytisch auf etwa 49 % der optimalen Energie in Graphen beschränkt, die Dreiecke enthalten (dieser Graph enthält das Dreieck 0-1-2). Der „Warmstart“ umgeht diese Einschränkung, indem er Vorwissen über die Lösung direkt in den Anfangszustand einkodiert.

# Number of QAOA layers (each layer = one cost unitary + one mixer unitary)
p = 1

# Regularization: clip c* to [epsilon, 1-epsilon] so no qubit is initialized
# in |0> or |1>, which would freeze it under the cost Hamiltonian.
epsilon = 0.25

c_clipped = np.clip(c_star, epsilon, 1 - epsilon)
thetas = 2 * np.arcsin(np.sqrt(c_clipped))

print(f"Continuous relaxation c*  = {np.round(c_star, 4)}")
print(f"After regularization      = {np.round(c_clipped, 4)}")
print(f"Warm-start angles theta   = {np.round(thetas, 4)} radians")
print()
print("Angle interpretation:")
print("  theta = 0      <->  c* = 0   (qubit points toward |0>)")
print(
    "  theta = pi/2   <->  c* = 0.5 (qubit in equal superposition, like |+>)"
)
print("  theta = pi     <->  c* = 1   (qubit points toward |1>)")

Output:

Continuous relaxation c*  = [1. 0. 0. 1.]
After regularization      = [0.75 0.25 0.25 0.75]
Warm-start angles theta   = [2.0944 1.0472 1.0472 2.0944] radians

Angle interpretation:
  theta = 0      <->  c* = 0   (qubit points toward |0>)
  theta = pi/2   <->  c* = 0.5 (qubit in equal superposition, like |+>)
  theta = pi     <->  c* = 1   (qubit points toward |1>)

Nach dem Clipping wird aus c∗=1c^* = 1 die Adresse 1−ε=0.751 - \varepsilon = 0.75 und aus c∗=0c^* = 0 die Adresse ε=0.25\varepsilon = 0.25. Die daraus resultierenden Winkel θ≈[2.09,1.05,1.05,2.09]\theta \approx [2.09, 1.05, 1.05, 2.09] Radiant drehen die Qubits 0 und 3 stark in Richtung ∣1⟩|1\rangle und die Qubits 1 und 2 in Richtung ∣0⟩|0\rangle, wodurch die Struktur des optimalen Schnitts direkt in den Anfangszustand des Quantenzustands kodiert wird.

def apply_cost_unitary(qc, cost_op, gamma):
    """Apply exp(-i * gamma * H_C) to the circuit.

    Each Pauli term in H_C contributes a rotation gate:
      - Single-Z term h_i * Z_i  ->  RZ(2 * gamma * h_i) on qubit i
      - Two-Z term J_ij * Z_i Z_j  ->  CNOT, RZ(2 * gamma * J_ij), CNOT
    """
    for pauli_term, coeff in zip(cost_op.paulis, cost_op.coeffs):
        indices = [
            j for j, q in enumerate(pauli_term.to_label()[::-1]) if q == "Z"
        ]
        if len(indices) == 1:
            qc.rz(2 * gamma * coeff.real, indices[0])
        elif len(indices) == 2:
            qc.cx(indices[0], indices[1])
            qc.rz(2 * gamma * coeff.real, indices[1])
            qc.cx(indices[0], indices[1])


def build_ws_qaoa(cost_op, n_layers, n_qubits, thetas):
    """WS-QAOA: warm-start initial state + custom per-qubit mixer.

    Per Egger et al. (2021) Eq. (1)-(2):
      Initial state per qubit i:  R_Y(theta_i) |0>
      Mixer gate per qubit i:     R_Y(theta_i) R_Z(-2*beta) R_Y(-theta_i)
    """
    gammas = ParameterVector("γ", n_layers)
    betas = ParameterVector("β", n_layers)
    qc = QuantumCircuit(n_qubits)
    for i, theta in enumerate(thetas):
        qc.ry(theta, i)  # warm-start initial state
    for k in range(n_layers):
        apply_cost_unitary(qc, cost_op, gammas[k])
        for i, theta in enumerate(thetas):
            qc.ry(theta, i)
            qc.rz(-2 * betas[k], i)
            qc.ry(-theta, i)
    return qc, gammas, betas


# Standard QAOA via the Qiskit built-in helper:
# qaoa_ansatz prepares |+>^n, then alternates exp(-i*gamma*H_C) with the
# default X-mixer for `reps` layers. The returned circuit exposes the
# variational parameters via std_qc.parameters.
std_qc = qaoa_ansatz(cost_operator, reps=p)

# WS-QAOA: keep the custom builder. The per-qubit mixer
# R_Y(theta_i) R_Z(-2*beta) R_Y(-theta_i) is implemented as an explicit gate
# sequence rather than as a SparsePauliOp, so we construct the circuit
# directly to stay close to the Egger et al. (2021) formulation.
ws_qc, ws_gammas, ws_betas = build_ws_qaoa(cost_operator, p, n_qubits, thetas)

Für den Standardansatz verweisen wir auf qaoa_ansatz, der „ ∣+⟩⊗n|+\rangle^{\otimes n} “ konstruiert, die Kosteneinheitsoperation anwendet und für jede der reps Schichten den Standard- XX -Mixer anwendet. Für WS-QAOA behalten wir den expliziten build_ws_qaoa Helfer bei, da der Mixer „ RY(θ) RZ(−2β) RY(−θ)R_Y(\theta)\,R_Z(-2\beta)\,R_Y(-\theta) “ pro Qubit als Gatterfolge und nicht als Summe von Pauli-Operatoren ausgedrückt wird. Der apply_cost_unitary Helfer liest direkt aus dem SparsePauliOp Hamilton-Operator aus und kann daher jedes QUBO-Problem ohne manuelle Schaltkreiskonstruktion lösen.

print("Standard QAOA circuit (p=1):")
std_qc.draw("mpl", fold=-1)

Output:

Standard QAOA circuit (p=1):
Output of the previous code cell
print("\nWS-QAOA circuit (p=1):")
ws_qc.draw("mpl", fold=-1)

Output:


WS-QAOA circuit (p=1):
Output of the previous code cell

Beide Schaltungen folgen dem gleichen Aufbau: eine Schicht zur Vorbereitung des Anfangszustands, gefolgt von pp abwechselnden cost-unitary- und mixer-unitary-Schichten. In der WS-QAOA-Schaltung kodieren die Anfangs- RYR_Y -Gatter c∗c^*, und der Mischer ersetzt jedes RXR_X durch ein konjugiertes Triplett aus RYR_Y – RZR_Z – RYR_Y. Der Unterschied in der Schaltungstiefe zwischen den beiden wächst linear mit pp, bleibt aber bei geringer Tiefe überschaubar.

Schritt 3: Mit „ Qiskit primitives “ ausführen

Wir verwenden StatevectorEstimator für eine exakte, rauschfreie Simulation. Die minimize Funktion von SciPy mit dem COBYLA-Optimierer steuert die Variationsschleife und ruft bei jeder Iteration den Schätzer auf, um ⟨HC⟩\langle H_C \rangle für einen gegebenen Parametersatz (γ,β)(\gamma, \beta) zu berechnen.

Die beiden Algorithmen verwenden unterschiedliche Anfangsparameter, die widerspiegeln, was jeder von ihnen vor der Optimierung weiß:

  • Standard-QAOA: Zufällige Initialisierung in „ [0,π][0, \pi] “ – angemessen, da keine Strukturinformationen vorliegen.
  • WS-QAOA: γ=0\gamma = 0, β=π/4\beta = \pi/4 — Bei γ=0\gamma=0 ist die Kosteneinheit die Identität, sodass die allererste Schaltungsauswertung direkt aus dem Warm-Start-Ausgangszustand erfolgt. Damit erhält COBYLA einen starken Startschuss, der mit der klassischen Lösung im Einklang steht.
estimator = StatevectorEstimator()


def make_cost_fn(circuit, param_order, cost_op, estimator, history):
    """Return a scalar cost function compatible with scipy.optimize.minimize."""

    def cost_fn(params):
        bound = circuit.assign_parameters(dict(zip(param_order, params)))
        job = estimator.run([(bound, cost_op)])
        energy = job.result()[0].data.evs.real
        history.append(energy)
        return energy

    return cost_fn


# Standard QAOA: random initialization
np.random.seed(42)
std_param_order = list(std_qc.parameters)
std_params0 = np.random.uniform(0, np.pi, len(std_param_order))
std_history = []

std_result = minimize(
    make_cost_fn(
        std_qc, std_param_order, cost_operator, estimator, std_history
    ),
    std_params0,
    method="COBYLA",
    options={"maxiter": 300, "rhobeg": 0.5},
)
print(f"Standard QAOA optimal energy : {std_result.fun:.4f}")
print(f"  optimal params: {std_result.x.round(4)}")
print(f"  optimizer calls: {len(std_history)}")


# WS-QAOA: informed initialization
ws_params0 = np.concatenate([np.zeros(p), np.full(p, np.pi / 4)])
ws_history = []
ws_param_order = list(ws_gammas) + list(ws_betas)

ws_result = minimize(
    make_cost_fn(ws_qc, ws_param_order, cost_operator, estimator, ws_history),
    ws_params0,
    method="COBYLA",
    options={"maxiter": 300, "rhobeg": 0.5},
)
print(f"\nWS-QAOA optimal energy       : {ws_result.fun:.4f}")
print(
    f"  optimal params: gamma={ws_result.x[:p].round(4)}, beta={ws_result.x[p:].round(4)}"
)
print(f"  optimizer calls: {len(ws_history)}")

Output:

Standard QAOA optimal energy : -0.5859
  optimal params: [0.6803 2.0533]
  optimizer calls: 47

WS-QAOA optimal energy       : -1.5000
  optimal params: gamma=[-0.0001], beta=[1.5708]
  optimizer calls: 42

Aufgrund des fundierten Ausgangspunkts von WS-QAOA beginnt COBYLA mit einem aussagekräftigen Energiewert nahe der Warmstart-Lösung, während Standard-QAOA von einem im Wesentlichen zufälligen Punkt auf der Energielandschaft ausgeht. Dieser Unterschied in der Ausgangsqualität ist der Hauptgrund für die in Schritt 4 sichtbare Konvergenzlücke.

# Compute the exact optimal energy by brute-force over all 2^n bitstrings
all_energies = [
    Statevector.from_label(format(k, f"0{n_qubits}b"))
    .expectation_value(cost_operator)
    .real
    for k in range(2**n_qubits)
]
optimal_energy = min(all_energies)

print(f"Exact optimal energy         : {optimal_energy:.4f}")
print(f"Standard QAOA approx. ratio  : {std_result.fun / optimal_energy:.4f}")
print(f"WS-QAOA approx. ratio        : {ws_result.fun / optimal_energy:.4f}")

Output:

Exact optimal energy         : -1.5000
Standard QAOA approx. ratio  : 0.3906
WS-QAOA approx. ratio        : 1.0000

Das Approximationsverhältnis ist definiert als „ ⟨HC⟩QAOA/Eopt\langle H_C \rangle_{\text{QAOA}} / E_{\text{opt}} “. Bei Minimierungsproblemen, bei denen „ Eopt<0E_{\text{opt}} < 0 “ gilt, bedeutet ein Verhältnis, das näher bei 1 liegt, dass der Algorithmus eine Lösung mit niedrigerer Energie (eine bessere Lösung) gefunden hat. Die Brute-Force-Suche über alle 2n2^n Basiszustände ist nur für kleine nn durchführbar und dient als Referenz für die tatsächlichen Werte.

Schritt 4: Nachbearbeitung und Ausgabe des Ergebnisses im gewünschten klassischen Format

Wir visualisieren die Konvergenz, untersuchen die optimierten Schaltkreise auf Bitstring-Lösungen, dekodieren diese Bitstrings zurück zu Max-Cut-Partitionen und fassen die Endergebnisse zusammen.

fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(std_history, label="Standard QAOA", alpha=0.85)
ax.plot(ws_history, label="WS-QAOA", alpha=0.85)
ax.axhline(
    optimal_energy,
    color="k",
    linestyle="--",
    label=f"Exact optimal ({optimal_energy:.2f})",
)
ax.set_xlabel("Optimizer call")
ax.set_ylabel(r"$\langle H_C \rangle$")
ax.set_title("Convergence: Standard QAOA vs. WS-QAOA")
ax.legend()
plt.tight_layout()
plt.show()

Output:

Output of the previous code cell

Das Konvergenzdiagramm zeigt den Energie ⟨HC⟩\langle H_C \rangle us bei jeder Auswertung der COBYLA-Funktion. Standard-QAOA unter p=1p=1 ist in diesem Graphen auf etwa 49 % der optimalen Energie begrenzt (das theoretische Maximum für p=1p=1 QAOA bei Graphen mit Dreiecken) und pendelt sich bei etwa −0.74-0.74 ein. WS-QAOA, das nahe der optimalen Lösung initialisiert wird, konvergiert schnell und mit weitaus weniger Iterationen nahe an −1.50-1.50 (das exakte Optimum). Dies verdeutlicht den entscheidenden Vorteil des Warmstarts: Bei gleicher Iterationstiefe erzielt er eine deutlich bessere Lösung.

# Sample the optimized circuits to recover the most probable bitstring solutions
sampler = StatevectorSampler()
shots = 1024


def get_best_bitstring(circuit, param_order, optimal_params, sampler, shots):
    bound = circuit.assign_parameters(dict(zip(param_order, optimal_params)))
    bound.measure_all()
    job = sampler.run([bound], shots=shots)
    counts = job.result()[0].data.meas.get_counts()
    return max(counts, key=counts.get), counts


def evaluate_cut(bitstring, G):
    """Compute the Max-Cut value for a bitstring node assignment."""
    x = [int(b) for b in bitstring]
    cut_val = sum(
        w for u, v, w in G.edges.data("weight", default=1) if x[u] != x[v]
    )
    set0 = [i for i, b in enumerate(bitstring) if b == "0"]
    set1 = [i for i, b in enumerate(bitstring) if b == "1"]
    return cut_val, set0, set1


# Qiskit bitstring ordering: rightmost character = qubit 0
def decode_bitstring(bs):
    return bs[::-1]


std_best, std_counts = get_best_bitstring(
    std_qc, std_param_order, std_result.x, sampler, shots
)
ws_best, ws_counts = get_best_bitstring(
    ws_qc, ws_param_order, ws_result.x, sampler, shots
)

std_cut, std_s0, std_s1 = evaluate_cut(decode_bitstring(std_best), G)
ws_cut, ws_s0, ws_s1 = evaluate_cut(decode_bitstring(ws_best), G)

print(f"Standard QAOA most-probable bitstring : {std_best}")
print(f"  Partition: S={std_s0}, S̄={std_s1}  |  cut value = {std_cut}")
print()
print(f"WS-QAOA most-probable bitstring       : {ws_best}")
print(f"  Partition: S={ws_s0}, S̄={ws_s1}  |  cut value = {ws_cut}")

Output:

Standard QAOA most-probable bitstring : 0110
  Partition: S=[0, 3], S̄=[1, 2]  |  cut value = 4.0

WS-QAOA most-probable bitstring       : 0110
  Partition: S=[0, 3], S̄=[1, 2]  |  cut value = 4.0

Bitstrings aus Sampler werden so zurückgegeben, dass sich das Qubit 0 ganz rechts befindet; daher ordnet die Umkehrung der Zeichenfolge den Index ii der Variablen xix_i zu. Der Schnittwert ist das Gesamtgewicht der Kanten, die die Partition kreuzen – genau das soll im Max-Cut-Problem maximiert werden. Ein Schnittwert von 4 nutzt vier der fünf verfügbaren Kanten, was dem theoretischen Maximum für diesen Graphen entspricht.

# Visualize the WS-QAOA solution on the graph
fig, axes = plt.subplots(1, 2, figsize=(8, 3))

for ax, s0, s1, cut, title in [
    (axes[0], std_s0, std_s1, std_cut, f"Standard QAOA (cut = {std_cut})"),
    (axes[1], ws_s0, ws_s1, ws_cut, f"WS-QAOA (cut = {ws_cut})"),
]:
    colors = ["skyblue" if i in s0 else "salmon" for i in G.nodes()]
    nx.draw(G, pos, with_labels=True, node_color=colors, ax=ax)
    nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, ax=ax)
    ax.set_title(title)

plt.tight_layout()
plt.show()

# Summary
# to_ising offset: QUBO value = Ising energy + offset, so Max-Cut value = -(Ising energy + offset)
optimal_cut = -(optimal_energy + offset)
print("=== Summary ===")
print(
    f"{'Method':<20} {'Ising energy':>14} {'Cut value':>12} {'Approx. ratio':>15}"
)
print("-" * 65)
print(
    f"{'Standard QAOA':<20} {std_result.fun:>14.4f} {std_cut:>12} {std_result.fun/optimal_energy:>15.4f}"
)
print(
    f"{'WS-QAOA':<20} {ws_result.fun:>14.4f} {ws_cut:>12} {ws_result.fun/optimal_energy:>15.4f}"
)
print(
    f"{'Exact optimal':<20} {optimal_energy:>14.4f} {optimal_cut:>12.0f} {'1.0000':>15}"
)

Output:

Output of the previous code cell
=== Summary ===
Method                 Ising energy    Cut value   Approx. ratio
-----------------------------------------------------------------
Standard QAOA               -0.5859          4.0          0.3906
WS-QAOA                     -1.5000          4.0          1.0000
Exact optimal               -1.5000            4          1.0000

In der grafischen Darstellung wird jeder Knoten entsprechend seiner Partitionszuordnung farblich gekennzeichnet (blau = SS, orange = Sˉ\bar{S} ). Kanten, die die Partition durchqueren (und unterschiedlich gefärbte Knoten verbinden), werden bei der Berechnung des Schnitts berücksichtigt.

Beide Methoden finden eine Bitfolge mit dem Schwellenwert 4, allerdings aus ganz unterschiedlichen Gründen. Es ist wichtig zu beachten, dass das Konvergenzdiagramm und die abgetastete Bitfolge zwei verschiedene Dinge messen :

  • Das Konvergenzdiagramm bildet die durchschnittliche Energie ⟨HC⟩\langle H_C \rangle s des gesamten Quantenzustands ab, also einen gewichteten Mittelwert über alle Bitfolgen in der Superposition. Das Standard-QAOA-Verfahren konvergiert gegen ~ −0.62-0.62, was deutlich über dem optimalen Wert von −1.50-1.50 liegt. Das bedeutet, dass sich sein Quantenzustand auf viele suboptimale Bitfolgen verteilt und nur gelegentlich die richtige Antwort enthält.
  • Die abgetastete Bitfolge ist eine einzelne Entnahme aus diesem Zustand. Standard-QAOA hatte hier Glück: Die optimale Partition war zufällig das am häufigsten auftretende Ergebnis, selbst ausgehend von einem diffusen Zustand. Bei schwierigeren Problemen, unzuverlässigerer Hardware oder einer größeren Anzahl konkurrierender Lösungsvorschläge reicht dieses Glück jedoch nicht mehr aus.

WS-QAOA hingegen konvergiert hinsichtlich seiner durchschnittlichen Energie vollständig zu −1.50-1.50, was bedeutet, dass sich sein Quantenzustand auf die optimalen Bitstrings konzentriert. Fast jeder Versuch liefert das richtige Ergebnis, sodass die Lösung zuverlässig und nicht durch Zufall gefunden wird.

Die praktische Konsequenz: Auf diesem kleinen, geräuschlosen Simulator mag der Unterschied gering erscheinen, doch bei größeren Problemumfängen oder auf echter Hardware ist ein Zustand, dessen durchschnittliche Energie nahe am Optimum liegt, weitaus robuster als einer, der nur gelegentlich die richtige Antwort aus einer diffusen Verteilung auswählt.

# Compare the full probability distribution over cut values for both
# algorithms. The most-probable bitstring above only reveals the mode;
# this histogram exposes how much of the quantum state's probability mass
# lands on the optimal cut versus on suboptimal partitions.
def cut_value_distribution(counts, G, shots):
    dist = {}
    for bs, c in counts.items():
        cut, _, _ = evaluate_cut(decode_bitstring(bs), G)
        dist[cut] = dist.get(cut, 0.0) + c / shots
    return dist


std_cut_dist = cut_value_distribution(std_counts, G, shots)
ws_cut_dist = cut_value_distribution(ws_counts, G, shots)

cut_values = sorted(set(std_cut_dist) | set(ws_cut_dist))
std_probs = [std_cut_dist.get(c, 0.0) for c in cut_values]
ws_probs = [ws_cut_dist.get(c, 0.0) for c in cut_values]

fig, ax = plt.subplots(figsize=(7, 4))
x = np.arange(len(cut_values))
width = 0.4
ax.bar(
    x - width / 2, std_probs, width, label="Standard QAOA", color="steelblue"
)
ax.bar(x + width / 2, ws_probs, width, label="WS-QAOA", color="salmon")
ax.axvline(
    cut_values.index(optimal_cut),
    color="k",
    linestyle="--",
    alpha=0.4,
    label=f"Optimal cut = {optimal_cut:g}",
)
ax.set_xticks(x)
ax.set_xticklabels([f"{c:g}" for c in cut_values])
ax.set_xlabel("Cut value")
ax.set_ylabel("Probability")
ax.set_title(f"Probability of measuring each cut value ({shots} shots)")
ax.legend()
plt.tight_layout()
plt.show()

print(
    f"P(cut = {optimal_cut:g}) | Standard QAOA = "
    f"{std_cut_dist.get(optimal_cut, 0):.4f}  "
    f"WS-QAOA = {ws_cut_dist.get(optimal_cut, 0):.4f}"
)

Output:

Output of the previous code cell
P(cut = 4) | Standard QAOA = 0.4639  WS-QAOA = 1.0000

Dieses Histogramm quantifiziert, was die Konvergenzdarstellung lediglich angedeutet hat. Bei der Standard-QAOA verteilt sich die Wahrscheinlichkeit auf mehrere suboptimale Schnittwerte, sodass die Wahrscheinlichkeit, bei einem einzelnen Versuch einen optimalen Schnitt von vier zu erhalten, nur einen Bruchteil der Gesamtmasse ausmacht. WS-QAOA konzentriert nahezu seine gesamte Wahrscheinlichkeit auf den optimalen Schnitt, sodass fast jeder Versuch die richtige Antwort liefert. Dies ist das praktische Unterscheidungsmerkmal eines Zustands, dessen durchschnittliche Energie sich der Grundzustandsenergie angenähert hat, im Gegensatz zu einem Zustand, bei dem der Grundzustand lediglich zufällig in einer breiten Überlagerung enthalten ist.

Beispiel für groß angelegte Hardware

Die Schritte 1–4 werden zu einem einzigen Codeblock zusammengefasst

# Selecting a backend using real hardware
service = QiskitRuntimeService()
backend = service.least_busy(
    operational=True, simulator=False, min_num_qubits=127
)
print(f"Using backend: {backend.name}")

Output:

Using backend: ibm_boston
# ── Step 1a: Build the 40-node Max-Cut problem ─────────────────────────────
# A 3-regular graph (every node has exactly 3 neighbors) is a standard QAOA
N_LARGE = 40
G_large = nx.random_regular_graph(d=3, n=N_LARGE, seed=0)
edges_large = list(G_large.edges())
print(f"Graph: {N_LARGE} nodes, {len(edges_large)} edges (3-regular)")

# Visualize the graph so it is clear what problem we are solving before any
# quantum work. Nodes in a circular layout; each edge contributes +1 to the
# cut value when its endpoints land in different partitions.
pos_large = nx.circular_layout(G_large)
fig, ax = plt.subplots(figsize=(6, 6))
nx.draw(
    G_large,
    pos_large,
    with_labels=True,
    node_color="lightblue",
    node_size=400,
    font_size=7,
    ax=ax,
)
ax.set_title(f"40-node 3-regular Max-Cut graph ({len(edges_large)} edges)")
plt.tight_layout()
plt.show()


# Same Maxcut → OptimizationProblem → QUBO → Ising pipeline as the small example,
# applied to the 40-node graph.
prob_large = Maxcut(G_large).to_optimization_problem()
converter_large = OptimizationProblemToQubo()
qubo_large = converter_large.convert(prob_large)
cost_op_large, offset_large = to_ising(qubo_large)
n_qubits_large = cost_op_large.num_qubits
print(
    f"Cost operator: {n_qubits_large} qubits, {len(cost_op_large)} Pauli terms"
)

# ── Step 1b: QP relaxation (multi-start L-BFGS-B) ─────────────────────────
# Same multi-start approach as the small example. At 40 qubits the relaxed
# landscape has many more local minima, so 200 random starts are essential
# to find a low-energy warm-start point.
Q_large = qubo_large.objective.quadratic.to_array(symmetric=True)
mu_large = qubo_large.objective.linear.to_array()


def qp_obj_large(x):
    return x @ Q_large @ x + mu_large @ x + qubo_large.objective.constant


bounds_large = [(0.0, 1.0)] * n_qubits_large
rng_qp = np.random.default_rng(42)
best_val_large, c_star_large = np.inf, None

for _ in range(200):
    x0 = rng_qp.uniform(0.0, 1.0, n_qubits_large)
    res = minimize(qp_obj_large, x0, method="L-BFGS-B", bounds=bounds_large)
    if res.fun < best_val_large:
        best_val_large, c_star_large = res.fun, res.x

# Regularize and convert to rotation angles (same formula as small example)
epsilon_large = 0.25
c_clipped_large = np.clip(c_star_large, epsilon_large, 1 - epsilon_large)
thetas_large = 2 * np.arcsin(np.sqrt(c_clipped_large))
print(
    f"c* range: [{c_star_large.min():.3f}, {c_star_large.max():.3f}]  "
    f"theta range: [{thetas_large.min():.3f}, {thetas_large.max():.3f}] rad"
)

# Plot the distribution of c* values to see how much structure the relaxation
# extracted. Values near 0/1 mean confident assignments; values near 0.5 mean
# the classical solver was uncertain and quantum exploration is most needed there.
fig, ax = plt.subplots(figsize=(6, 3))
ax.hist(c_star_large, bins=20, color="steelblue", edgecolor="white")
ax.axvline(0.5, color="k", linestyle="--", label="Uniform prior (std QAOA)")
ax.set_xlabel(r"$c^*_i$")
ax.set_ylabel("Count")
ax.set_title(r"Distribution of warm-start values $c^*_i$ (40-node graph)")
ax.legend()
plt.tight_layout()
plt.show()

# ── Step 1c: Build WS-QAOA circuit ─────────────────────────────────────────
# Reuse build_ws_qaoa from the small-scale section unchanged; the helper
# scales automatically with n_qubits and the cost operator size.
p_large = 1
ws_qc_large, ws_gammas_large, ws_betas_large = build_ws_qaoa(
    cost_op_large, p_large, n_qubits_large, thetas_large
)
ws_qc_large.measure_all()

# ── Step 2: Transpile to hardware-native gates ──────────────────────────
# generate_preset_pass_manager compiles the abstract circuit to th
# gate set of the backend and inserts SWAP gates wherever the cost Hamiltonian
# couples qubits that are not directly connected on the processor.
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
ws_isa_large = pm.run(ws_qc_large)

ecr_count = ws_isa_large.count_ops().get("ecr", 0)
print(
    f"\nTranspiled circuit: 2Q depth={ws_isa_large.depth(lambda x: x.operation.num_qubits == 2)}"
)
ws_isa_large.draw("mpl", fold=-1)

Output:

Graph: 40 nodes, 60 edges (3-regular)
Output of the previous code cell
Cost operator: 40 qubits, 60 Pauli terms
c* range: [0.000, 1.000]  theta range: [1.047, 2.094] rad
Output of the previous code cell

Transpiled circuit: 2Q depth=86
Output of the previous code cell
# ── Classical baseline via simulated annealing ────────────────────
# Run SA before any hardware calls to get a strong classical reference cut
# value. SA is fast (seconds), needs no solver license, and reliably finds
# near-optimal solutions on 40-node graphs. We use sa_cut as the denominator
# for the approximation ratio instead of the looser QP upper bound.
#
# At each step we flip a random node and accept the move if it improves the
# cut, or with probability exp(delta/T) otherwise. Temperature T decays
# geometrically, allowing uphill moves early on to escape local minima.
def simulated_annealing_maxcut(
    G, seed=0, T0=2.0, T_min=1e-4, alpha=0.995, n_steps=100_000
):
    rng_sa = np.random.default_rng(seed)
    n = G.number_of_nodes()
    x = rng_sa.integers(0, 2, n)
    best_x = x.copy()
    best_cut = sum(1 for u, v in G.edges() if x[u] != x[v])
    T = T0
    for _ in range(n_steps):
        i = rng_sa.integers(0, n)
        delta = sum((-1 if x[i] != x[nb] else 1) for nb in G.neighbors(i))
        if delta > 0 or rng_sa.random() < np.exp(delta / T):
            x[i] ^= 1
            cut = sum(1 for u, v in G.edges() if x[u] != x[v])
            if cut > best_cut:
                best_cut, best_x = cut, x.copy()
        T = max(T * alpha, T_min)
    return best_x, best_cut


sa_solution, sa_cut = simulated_annealing_maxcut(G_large)
print(f"Simulated annealing cut value: {sa_cut}  (classical reference)")

# ── Step 3: Execution on hardware ───────────────────────────
# A Session reserves the backend so the COBYLA iterations and final sampling
# run back-to-back without re-queuing between jobs — important when the
# optimizer submits many short jobs sequentially. All jobs are tagged with
# "TUT_WSQAOA" for traceability in the IBM Quantum dashboard.
#
# EstimatorV2 with resilience_level=1 enables twirled readout error extinction
# (TREX), which corrects systematic measurement bit-flip errors without extra
# circuit overhead. 4096 shots per call balances estimation noise vs. job time.
estimator_options = EstimatorOptions()
estimator_options.resilience_level = 1
estimator_options.default_shots = 4096
estimator_options.environment.job_tags = ["TUT_WSQAOA"]

# Align the cost observable with the physical qubit layout chosen by the transpiler
cost_op_isa = cost_op_large.apply_layout(ws_isa_large.layout)
ws_param_order_isa = list(ws_isa_large.parameters)

ws_history_hw = []

with Session(backend=backend) as session:
    estimator_hw = Estimator(mode=session, options=estimator_options)

    def hw_cost_fn(params):
        bound = ws_isa_large.assign_parameters(
            dict(zip(ws_param_order_isa, params))
        )
        energy = (
            estimator_hw.run([(bound, cost_op_isa)]).result()[0].data.evs.real
        )
        ws_history_hw.append(float(energy))
        print(
            f"  iter {len(ws_history_hw):>3d}  <H_C> = {energy:.4f}", end="\r"
        )
        return float(energy)

    # Warm-start initialization: gamma=0 means the cost unitary is the identity on
    # the first call, so COBYLA immediately evaluates the warm-start state itself —
    # a much better starting signal than a random point.
    ws_params0_hw = np.concatenate(
        [np.zeros(p_large), np.full(p_large, np.pi / 4)]
    )

    ws_result_hw = minimize(
        hw_cost_fn,
        ws_params0_hw,
        method="COBYLA",
        options={"maxiter": 150, "rhobeg": 0.3},
    )
    print(
        f"\nOptimization complete: energy={ws_result_hw.fun:.4f}, "
        f"iterations={len(ws_history_hw)}"
    )

    # ── Step 3b: Sample the optimized circuit ──────────────────────────────────
    # Use 8192 shots for the final sample to get a reliable mode estimate.
    sampler_hw = Sampler(
        mode=session,
        options={"environment": {"job_tags": ["TUT_WSQAOA"]}},
    )
    ws_bound_hw = ws_isa_large.assign_parameters(
        dict(zip(ws_param_order_isa, ws_result_hw.x))
    )
    counts_hw = (
        sampler_hw.run([ws_bound_hw], shots=8192)
        .result()[0]
        .data.meas.get_counts()
    )

best_bs_hw = max(counts_hw, key=counts_hw.get)
best_count = counts_hw[best_bs_hw]
total_shots = sum(counts_hw.values())

# Decode: Qiskit returns bitstrings with qubit 0 at the rightmost position,
# so reversing the string maps character index i to variable x_i.
cut_val_hw, s0_hw, s1_hw = evaluate_cut(best_bs_hw[::-1], G_large)

# Compare against simulated annealing.
# A ratio >= 1.0 means WS-QAOA matched or beat the classical SA solution.
# A ratio close to 1.0 (e.g. > 0.95) shows the quantum result is competitive.
approx_ratio_hw = cut_val_hw / sa_cut
print(
    f"Most-probable bitstring frequency: {best_count}/{total_shots} "
    f"({100*best_count/total_shots:.1f}%)"
)
print(
    f"WS-QAOA cut: {cut_val_hw}  |  SA cut: {sa_cut}  "
    f"|  Approximation ratio vs SA: {approx_ratio_hw:.4f}"
)

# Visualize both solutions side-by-side on the graph.
# Blue = partition S, orange = partition S-bar.
# Edges crossing between colors are the ones counted in the cut.
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
for ax, assignment, cut, title in [
    (
        axes[0],
        list(sa_solution),
        sa_cut,
        f"Simulated Annealing (cut={sa_cut})",
    ),
    (
        axes[1],
        [int(b) for b in best_bs_hw[::-1]],
        cut_val_hw,
        f"WS-QAOA hardware (cut={cut_val_hw})",
    ),
]:
    colors = [
        "skyblue" if assignment[i] == 0 else "salmon" for i in G_large.nodes()
    ]
    nx.draw(
        G_large,
        pos_large,
        with_labels=True,
        node_color=colors,
        node_size=400,
        font_size=7,
        ax=ax,
    )
    ax.set_title(title)
plt.suptitle("Max-Cut partitions: SA vs WS-QAOA", fontsize=13)
plt.tight_layout()
plt.show()

# ── Step 4: Convergence plot and summary ──────────────────────────────────
# On real hardware the trace will be noisy (shot noise + gate errors), but the
# overall downward trend confirms that COBYLA is making progress despite noise.
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(ws_history_hw, color="tab:orange", label="WS-QAOA (hardware)")
ax.axhline(
    ws_result_hw.fun,
    color="tab:orange",
    linestyle=":",
    label=f"Final energy ({ws_result_hw.fun:.3f})",
)
ax.set_xlabel("Optimizer call")
ax.set_ylabel(r"$\langle H_C \rangle$")
ax.set_title(f"WS-QAOA convergence on {backend.name} (40 qubits, p=1)")
ax.legend()
plt.tight_layout()
plt.show()


print("\n=== Large Scale Summary ===")
print(f"{'Metric':<38} {'Value':>10}")
print("-" * 50)
print(f"{'Nodes / Edges':<38} {N_LARGE:>5} / {len(edges_large):<4}")
print(f"{'QAOA layers (p)':<38} {p_large:>10}")
print(f"{'Transpiled ECR gate count':<38} {ecr_count:>10}")
print(f"{'Transpiled circuit depth':<38} {ws_isa_large.depth():>10}")
print(f"{'Optimizer iterations':<38} {len(ws_history_hw):>10}")
print(f"{'WS-QAOA energy (hardware)':<38} {ws_result_hw.fun:>10.4f}")
print(f"{'Cut value':<38} {cut_val_hw:>10}")
print(f"{'Simulated annealing cut value':<38} {sa_cut:>10}")
print(f"{'Approximation ratio (vs SA)':<38} {approx_ratio_hw:>10.4f}")

Output:

Simulated annealing cut value: 53  (classical reference)
  iter  31  <H_C> = -12.4094
Optimization complete: energy=-13.0256, iterations=31
Most-probable bitstring frequency: 4/8192 (0.0%)
WS-QAOA cut: 53  |  SA cut: 53  |  Approximation ratio vs SA: 1.0000
Output of the previous code cell Output of the previous code cell

=== Large Scale Summary ===
Metric                                      Value
--------------------------------------------------
Nodes / Edges                             40 / 60  
QAOA layers (p)                                 1
Transpiled ECR gate count                       0
Transpiled circuit depth                      276
Optimizer iterations                           31
WS-QAOA energy (hardware)                -13.0256
Cut value                                      53
Simulated annealing cut value                  53
Approximation ratio (vs SA)                1.0000

Nächste Schritte

Empfehlungen

Wenn Sie diese Arbeit interessant fanden, könnten Sie auch an folgenden Materialien interessiert sein:

  • Höhere QAOA-Schichten : Vergrößern p Sie die Darstellung, um zu sehen, wie sich beide Algorithmen mit zunehmender Schichtenanzahl verbessern und ob der Vorteil von WS-QAOA bei geringer Schichtenanzahl bestehen bleibt.
  • Qiskit-Add-on „Optimization Mapper “: Sehen Sie sich die Dokumentation an und probieren Sie aus, verschiedene kombinatorische Probleme zu modellieren oder unterschiedliche Solver für die kontinuierliche Relaxation zu verwenden.

Referenzen

[1] D. J. Egger, J. Mareček und S. Woerner, „Warm-starting quantum optimization“, Quantum, Band 5, S. 479, 2021. arXiv:2009.10095

[2] E. Farhi, J. Goldstone und S. Gutmann, „Ein algorithmischer Ansatz zur approximativen Quantenoptimierung“, arXiv:1411.4028, 2014.

War diese Seite hilfreich?
Melden Sie auf GitHub einen Fehler, einen Tippfehler oder fordern Sie Inhalte an.