Skip to main content
IBM Quantum Platform

QAOA con avvio a caldo tramite l'add-on Optimization Mapper di Qiskit

Stima della durata: 9 minuti su un Heron r3 (NOTA: si tratta solo di una stima. (La durata potrebbe variare.)


Risultati di apprendimento

Al termine di questo tutorial, avrai acquisito le seguenti conoscenze:

  • Come mappare un problema di taglio massimo in una formulazione quantistica di ottimizzazione binaria quadratica senza vincoli (QUBO) utilizzando qiskit-addon-opt-mapper
  • Come implementare ed eseguire il QAOA standard su un simulatore
  • Come applicare il metodo WS-QAOA calcolando il rilassamento del programma quadratico (QP) e realizzando il circuito di warm-start
  • Come confrontare la convergenza energetica e la qualità della soluzione tra il QAOA standard e il WS-QAOA

Prerequisiti

Si consiglia di approfondire i seguenti argomenti:


Sfondo

L'algoritmo di ottimizzazione approssimativa quantistica (QAOA) è un algoritmo ibrido quantistico-classico progettato per risolvere problemi di ottimizzazione combinatoria quali il max-cut e le formulazioni QUBO generali. Per un'introduzione di base al QAOA in Qiskit, consulta il tutorial sul QAOA; per tecniche più avanzate di progettazione dei circuiti, consulta il tutorial avanzato sul QAOA.

Nel QAOA standard:

  • Lo stato iniziale è la sovrapposizione uniforme +n|+\rangle^{\otimes n}.
  • I parametri variazionali vengono inizializzati in modo casuale.
  • Un ottimizzatore classico cerca i parametri che minimizzano la funzione di costo.

Tuttavia, in presenza di problemi di dimensioni realistiche e di hardware quantistico soggetto a rumore, l’inizializzazione casuale può comportare una convergenza lenta, minimi locali sfavorevoli e un aumento del costo di ottimizzazione.

Il QAOA con avvio a caldo (WS-QAOA) migliora questo aspetto incorporando direttamente nel circuito quantistico i principi dell'ottimizzazione classica. Questo tutorial segue i metodi introdotti da Egger, Mareček e Woerner nell'articolo " Warm-starting quantum optimization ". L'idea fondamentale è quella di:

  1. Risolvere un rilassamento continuo del problema binario originale (un programma quadratico su [0,1]n[0,1]^n anziché {0,1}n\{0,1\}^n ).
  2. Codificare la soluzione rilassata ci[0,1]c^*_i \in [0,1] in uno stato iniziale personalizzato utilizzando YY - angoli di rotazione θi=2arcsin(ci)\theta_i = 2\arcsin(\sqrt{c^*_i}), in modo che il qubit ii inizi in uno stato la cui probabilità di misurare 1|1\rangle sia pari a cic^*_i.
  3. Sostituire il mixer standard XX con un mixer personalizzato il cui stato di base sia lo stato iniziale di “warm-start”, assicurando che l’algoritmo parta in prossimità della soluzione classica e possa esplorare il vicinato.

Un parametro di regolarizzazione ε[0,0.5]\varepsilon \in [0, 0.5] limita cic^*_i lontano da 0 e 1 per evitare problemi di raggiungibilità; i qubit inizializzati in 0|0\rangle o 1|1\rangle non possono essere spostati dall'Hamiltoniano di costo. All'indirizzo ε=0.5\varepsilon = 0.5, il WS-QAOA si riduce esattamente al QAOA standard.

La modellizzazione del problema utilizza il qiskit-addon-opt-mapper pacchetto, la cui Maxcut classe di applicazione costruisce il QUBO direttamente da un grafo, mentre i suoi convertitori e traduttori mappano il problema risultante agli hamiltoniani quantistici.


Requisiti

Prima di iniziare questo tutorial, assicurati di avere installato quanto segue:

  • Qiskit SDK v2.0 o versioni successive, con supporto alla visualizzazione
  • Qiskit Runtime v0.43 o versioni successive (pip install qiskit-ibm-runtime)
  • Componente aggiuntivo "Optimization Mapper" per Qiskit (pip install qiskit-addon-opt-mapper)
  • SciPy (pip install scipy)
  • NetworkX (pip install networkx)

Configura

Importa tutte le librerie necessarie e definisci le funzioni di supporto utilizzate nel corso di questo tutorial.

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

Esempio di simulatore su piccola scala

Come esempio pratico utilizzeremo un problema di “max-cut” su un grafo ponderato. Max-cut chiede: dato un grafo G=(V,E)G=(V,E) con pesi degli archi wijw_{ij}, trovare una partizione dei vertici in due insiemi SS e Sˉ\bar{S} che massimizzi il peso totale degli archi che attraversano il taglio.

In quanto problema di minimizzazione QUBO, il max-cut può essere espresso come: minx{0,1}n(i,j)Ewij(xi+xj2xixj)\min_{x \in \{0,1\}^n} -\sum_{(i,j) \in E} w_{ij}(x_i + x_j - 2x_i x_j)

Per garantire la gestibilità su un simulatore, utilizziamo un grafo a quattro nodi.

Fase 1: Mappare gli input classici su un problema quantistico

Definiamo il problema del taglio massimo utilizzando la Maxcut classe di applicazioni di qiskit-addon-opt-mapper, che costruisce la formulazione QUBO direttamente a partire da un grafo. Lo convertiamo quindi in un QUBO e lo traduciamo in un hamiltoniano di Ising (SparsePauliOp) adatto al QAOA. Risolviamo inoltre il rilassamento continuo del QUBO — sostituendo il vincolo binario xi{0,1}x_i \in \{0,1\} con xi[0,1]x_i \in [0,1] — per ottenere il punto iniziale di warm-start cc^*.

# 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

Il grafo ha cinque spigoli. La partizione “max-cut” ottimale suddivide i nodi in S={0,3}S = \{0, 3\} e Sˉ={1,2}\bar{S} = \{1, 2\} (o il suo complemento), tagliando quattro dei cinque spigoli, con un valore di taglio pari a 4.

# 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

La Maxcut classe implementa la costruzione QUBO, così non dobbiamo espandere manualmente l'obiettivo del taglio massimo. L'obiettivo stampato mostra il coefficiente lineare di ciascuna variabile (quanto contribuisce individualmente al taglio) e il coefficiente quadratico di ciascun termine incrociato (la penalità per aver collocato due nodi adiacenti sullo stesso lato). Il valore sottostante OptimizationProblem restituito da to_optimization_problem() supporta variabili binarie, intere, continue e di tipo spin, ed è lo stesso oggetto richiesto dai convertitori e dai traduttori utilizzati nella fase successiva.

# 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

Il to_ising traduttore restituisce un SparsePauliOp valore che rappresenta HCH_C e uno scalare offset tale che QUBO value=HC+offset\text{QUBO value} = \langle H_C \rangle + \text{offset}. Per questo problema del taglio massimo con pesi unitari, hi=0h_i = 0 per tutti i qubit (il grafo è simmetrico in termini lineari dopo la sostituzione xizix_i \to z_i ), e ogni arco contribuisce con un accoppiamento ZiZjZ_i Z_j di intensità +0.5+0.5. L’autovalore minimo di HCH_C corrisponde al taglio massimo.

# 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

Il risolutore multi-start individua c=[1,0,0,1]c^* = [1, 0, 0, 1] (o il suo complemento [0,1,1,0][0, 1, 1, 0] ), che rappresenta la soluzione binaria effettivamente ottimale. Per questo problema, il rilassamento QP è stretto; il minimo continuo coincide con l'ottimo intero, il che significa che il rilassamento identifica immediatamente il taglio migliore. Dopo la regolarizzazione con ε=0.25\varepsilon = 0.25 nella Fase 2, questa soluzione verrà codificata nello stato iniziale di warm-start.

Fase 2: Ottimizzare il problema per l'esecuzione su hardware quantistico

Realizziamo due circuiti QAOA e prepariamo gli angoli di warm-start a partire dalla soluzione QP.

Il modello QAOA standard utilizza la sovrapposizione uniforme +n|+\rangle^{\otimes n} come stato iniziale e il mixer standard XX -mixer HM=iXiH_M = -\sum_i X_i, implementato come iRX(2β)\prod_i R_X(-2\beta) per ogni strato.

Il QAOA con avvio a caldo (WS-QAOA) descritto in [1] apporta due modifiche strutturali per ogni qubit ii :

  • Stato iniziale: RY(θi)0R_Y(\theta_i)|0\rangle con θi=2arcsin(ci)\theta_i = 2\arcsin(\sqrt{c^*_i}), quindi la probabilità di misurare 1|1\rangle è pari a cic^*_i.
  • Mixer personalizzato: RY(θi)RZ(2β)RY(θi)R_Y(\theta_i)\, R_Z(-2\beta)\, R_Y(-\theta_i), il cui stato fondamentale è RY(θi)0R_Y(\theta_i)|0\rangle. Ciò significa che il WS-QAOA parte dallo stato fondamentale del proprio mixer, la stessa proprietà che il QAOA standard soddisfa con l’ +|+\rangle e e il mixer XX.

Nota sui livelli: nel p=1 caso di un singolo livello QAOA, il QAOA standard è analiticamente limitato a circa il 49% dell'energia ottimale su grafi contenenti triangoli (questo grafo presenta il triangolo 0-1-2). Il "warm start" aggira questa limitazione codificando la conoscenza a priori della soluzione direttamente nello stato iniziale.

# 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>)

Dopo il taglio, c=1c^* = 1 diventa 1ε=0.751 - \varepsilon = 0.75 e c=0c^* = 0 diventa ε=0.25\varepsilon = 0.25. Gli angoli risultanti θ[2.09,1.05,1.05,2.09]\theta \approx [2.09, 1.05, 1.05, 2.09] radianti ruotano i qubit 0 e 3 fortemente verso 1|1\rangle e i qubit 1 e 2 verso 0|0\rangle, codificando direttamente la struttura del taglio ottimale nello stato quantistico iniziale.

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)

Per l'approccio standard ci rimandiamo a qaoa_ansatz, che costruisce un +n|+\rangle^{\otimes n} e, applica l'unitario di costo e utilizza il mixer predefinito XX per ciascuno dei reps strati. Per WS-QAOA manteniamo l'helper esplicito build_ws_qaoa poiché il mixer per singolo qubit RY(θ)RZ(2β)RY(θ)R_Y(\theta)\,R_Z(-2\beta)\,R_Y(-\theta) è espresso come sequenza di gate anziché come somma di operatori di Pauli. apply_cost_unitary L'helper legge direttamente dall'Hamiltoniano SparsePauliOp , quindi è in grado di gestire qualsiasi problema QUBO senza dover costruire manualmente il circuito.

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

Entrambi i circuiti seguono la stessa struttura: uno strato iniziale di preparazione dello stato, seguito d pp i strati alternati di tipo “cost-unitary” e “mixer-unitary”. Nel circuito WS-QAOA, i gate di apertura RYR_Y codificano cc^*, mentre il mixer sostituisce ogni RXR_X con una tripletta coniugata RYR_YRZR_ZRYR_Y. La differenza di profondità del circuito tra i due cresce in modo lineare con l' pp e, ma rimane gestibile a basse profondità.

Fase 3: Eseguire il comando utilizzando Qiskit primitives

Utilizziamo StatevectorEstimator per una simulazione esatta e priva di rumore. La minimize funzione disponibile all'indirizzo SciPy, che utilizza l'ottimizzatore COBYLA, gestisce il ciclo variazionale, richiamando lo stimatore ad ogni iterazione per calcolare HC\langle H_C \rangle per un dato insieme di parametri (γ,β)(\gamma, \beta).

I due algoritmi utilizzano parametri iniziali diversi che riflettono le informazioni di cui ciascuno dispone prima dell'ottimizzazione:

  • QAOA standard: inizializzazione casuale in [0,π][0, \pi] — appropriata poiché non sono disponibili informazioni strutturali.
  • WS-QAOA: γ=0\gamma = 0, β=π/4\beta = \pi/4 — su γ=0\gamma=0 il costo unitario è l'identità, quindi la primissima valutazione del circuito preleva campioni direttamente dallo stato iniziale di warm-start. Ciò fornisce a COBYLA un solido punto di partenza in linea con la soluzione classica.
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

Il punto di partenza ben definito di WS-QAOA fa sì che COBYLA inizi con un valore energetico significativo vicino alla soluzione di avvio a caldo, mentre il QAOA standard parte da un punto sostanzialmente casuale sul panorama energetico. Questa differenza nella qualità iniziale è il fattore principale all'origine del divario di convergenza visibile nella Fase 4.

# 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

Il rapporto di approssimazione è definito come HCQAOA/Eopt\langle H_C \rangle_{\text{QAOA}} / E_{\text{opt}}. Per i problemi di minimizzazione in cui Eopt<0E_{\text{opt}} < 0, un rapporto più vicino a 1 indica che l'algoritmo ha trovato un'energia inferiore (una soluzione migliore). La ricerca con metodo brute-force su tutti gli stati di base di tipo “ 2n2^n ” è fattibile solo per valori ridotti di nn e funge da riferimento di riferimento.

Fase 4: Elaborazione finale e restituzione del risultato nel formato classico desiderato

Visualizziamo la convergenza, campioniamo i circuiti ottimizzati per le soluzioni sotto forma di stringhe di bit, decodifichiamo tali stringhe di bit per ricavarne partizioni di taglio massimo e riassumiamo i risultati finali.

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

Il grafico di convergenza mostra l' HC\langle H_C \rangle e energetica ad ogni valutazione della funzione COBYLA. Il QAOA standard all'indirizzo p=1p=1 è limitato a circa il 49% dell'energia ottimale su questo grafo (il massimo teorico per un QAOA di tipo " p=1p=1 " su grafi con triangoli), attestandosi intorno a 0.74-0.74. Il WS-QAOA, inizializzato in prossimità della soluzione ottimale, converge rapidamente a un valore vicino a 1.50-1.50 (l'ottimo esatto) con un numero di iterazioni di gran lunga inferiore. Ciò dimostra il vantaggio fondamentale del warm start: a parità di profondità del circuito, consente di ottenere una soluzione significativamente migliore.

# 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

Le stringhe di Sampler bit vengono restituite con il qubit 0 nella posizione più a destra; pertanto, invertendo la stringa, l'indice ii viene mappato sulla variabile xix_i. Il valore del taglio è il peso totale dei bordi che attraversano la partizione, ovvero ciò che il problema del taglio massimo mira a massimizzare. Un valore di taglio pari a 4 utilizza quattro dei cinque spigoli disponibili, che rappresenta il massimo teorico per questo grafo.

# 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

Nella visualizzazione grafica, ogni nodo è colorato in base alla partizione a cui è assegnato (blu = SS, arancione = Sˉ\bar{S} ). I bordi che attraversano la partizione (collegando nodi di colore diverso) sono quelli conteggiati nel taglio.

Entrambi i metodi individuano una stringa di bit con valore di taglio pari a 4, ma per ragioni molto diverse. È importante sottolineare che il grafico di convergenza e la stringa di bit campionata misurano due cose diverse :

  • Il grafico di convergenza traccia l' HC\langle H_C \rangle e energetica media dello stato quantistico completo, ovvero una media ponderata su tutte le stringhe di bit presenti nella sovrapposizione. Il QAOA standard converge verso ~ 0.62-0.62, un valore ben al di sopra dell’ottimale 1.50-1.50, il che significa che il suo stato quantistico è distribuito su molte stringhe di bit subottimali e solo occasionalmente include la risposta corretta.
  • La stringa di bit campionata rappresenta un singolo prelievo da quello stato. In questo caso, l'algoritmo QAOA standard ha avuto fortuna: la partizione ottimale si è rivelata essere l'esito campionato con maggiore frequenza anche a partire da uno stato diffuso. Quando si ha a che fare con problemi più complessi, hardware più rumoroso o un numero maggiore di soluzioni candidate in competizione tra loro, questa fortuna finisce.

WS-QAOA, al contrario, fa convergere la propria energia media interamente verso 1.50-1.50, il che significa che il suo stato quantistico è concentrato sulle stringhe di bit ottimali. Quasi ogni tentativo fornisce la risposta corretta, quindi la soluzione viene individuata in modo affidabile e non per caso.

La conseguenza pratica: su questo piccolo simulatore silenzioso la differenza può sembrare minima, ma quando i problemi sono di dimensioni maggiori o su hardware reale, uno stato con un’energia media vicina a quella ottimale è di gran lunga più robusto di uno che solo occasionalmente estrae la risposta corretta da una distribuzione diffusa.

# 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

Questo istogramma quantifica ciò che il grafico di convergenza aveva solo accennato. La probabilità del QAOA standard è distribuita su più valori di taglio subottimali, quindi la probabilità di ottenere un taglio ottimale pari a quattro in un singolo tentativo rappresenta solo una frazione della massa totale. WS-QAOA concentra quasi tutta la sua probabilità sul taglio ottimale, quindi quasi ogni tentativo restituisce la risposta corretta. Questa è la caratteristica distintiva di uno stato la cui energia media è convergita verso l'energia dello stato fondamentale, rispetto a uno stato che ha semplicemente incluso lo stato fondamentale in un'ampia sovrapposizione.

Esempio di hardware su larga scala

I passaggi da 1 a 4 sono raggruppati in un unico blocco di codice

# 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

Passi successivi

Suggerimenti

Se questo lavoro ti è sembrato interessante, potrebbero interessarti anche i seguenti contenuti:

  • Strati QAOA superiori : aumentare p il numero di strati per osservare come entrambi gli algoritmi migliorino con l’aumentare del numero di strati del circuito e verificare se il vantaggio del WS-QAOA a bassa profondità persista.
  • Mapper di ottimizzazione dell'add-on Qiskit : consulta la documentazione e prova a modellare diversi problemi combinatori oppure a utilizzare diversi risolutori per il rilassamento continuo.

Riferimenti

[1] D. J. Egger, J. Mareček e S. Woerner, "Warm-starting quantum optimization", Quantum, vol. 5, p. 479, 2021. arXiv:2009.10095

[2] E. Farhi, J. Goldstone e S. Gutmann, "Un algoritmo quantistico di ottimizzazione approssimativa", * arXiv:1411.4028 *, 2014.

Questa pagina è stata utile?
Segnala un bug, un errore di battitura o richiedi contenuti su GitHub.