Aller au contenu principal

Algorithme d'optimisation approximative quantique

Estimation d'utilisation : 22 minutes sur un processeur Heron r3 (REMARQUE : il s'agit uniquement d'une estimation. Ton temps d'exécution peut varier.)

Objectifs d'apprentissage

Après avoir terminé ce tutoriel, tu peux t'attendre à comprendre les informations suivantes :

  • Comment transposer un problème classique d'optimisation combinatoire (max-cut) en un hamiltonien quantique
  • Comment implémenter et exécuter l'algorithme d'optimisation approximative quantique (QAOA) à l'aide des sessions Qiskit Runtime
  • Comment faire passer un flux de travail QAOA d'un petit exemple sur simulateur à une exécution matérielle à l'échelle utilitaire

Prérequis

Il est recommandé de te familiariser avec ces sujets :

Contexte

L'algorithme d'optimisation approximative quantique (QAOA) est une méthode itérative hybride quantique-classique pour résoudre des problèmes d'optimisation combinatoire. Dans ce tutoriel, tu vas utiliser QAOA pour résoudre le problème de la coupe maximale (max-cut) — un problème d'optimisation NP-difficile avec des applications en clustering, en science des réseaux et en physique statistique. Étant donné un graphe de nœuds connectés par des arêtes, l'objectif est de partitionner les nœuds en deux ensembles de manière à maximiser le nombre d'arêtes traversant la partition.

Illustration d'un problème de coupe maximale

De l'optimisation classique aux circuits quantiques

Max-cut peut être exprimé comme un problème d'optimisation binaire classique. Chaque nœud se voit attribuer une variable binaire xi{0,1}x_i \in \{0, 1\} indiquant à quel ensemble il appartient. L'objectif est de maximiser le nombre d'arêtes dont les extrémités sont dans des ensembles différents :

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

Il s'agit de manière équivalente d'un problème d'optimisation binaire quadratique sans contraintes (QUBO) de la forme minxxTQx\min_x\, x^T Q x. Grâce à une substitution de variable standard (xi(1Zi)/2x_i \to (1 - Z_i)/2), le QUBO peut être réécrit sous la forme d'un hamiltonien de coût dont l'état fondamental encode la solution optimale. En général, cet hamiltonien comporte à la fois des termes quadratiques et linéaires :

HC=ijQijZiZj+ibiZi.H_C = \sum_{ij} Q_{ij} \, Z_i Z_j + \sum_i b_i \, Z_i.

Pour le problème max-cut non pondéré considéré ici, les coefficients linéaires s'annulent (bi=0b_i = 0) et Qij=1Q_{ij} = 1 pour chaque arête, ce qui laisse la forme plus simple HC=(i,j)EZiZjH_C = \sum_{(i,j) \in E} Z_i Z_j que tu construiras dans le code ci-dessous. La forme plus générale ci-dessus est ce dont tu aurais besoin pour adapter ce flux de travail à des graphes pondérés ou à d'autres problèmes exprimables sous forme de QUBO.

Comment fonctionne QAOA

QAOA prépare des solutions candidates en appliquant des couches alternées de deux opérateurs à un état de superposition initial Hn0H^{\otimes n}|0\rangle : l'opérateur de coût eiγkHCe^{-i\gamma_k H_C} et un opérateur de mélange eiβkHme^{-i\beta_k H_m}. Les angles γk\gamma_k et βk\beta_k sont optimisés dans une boucle de rétroaction classique ; l'ordinateur quantique évalue la fonction de coût, et un optimiseur classique met à jour les paramètres jusqu'à convergence. Cette boucle itérative s'exécute au sein d'une session Qiskit Runtime, qui maintient le dispositif quantique réservé d'une itération à l'autre pour réduire la latence.

Diagramme de circuit avec des couches QAOA

Pour un traitement plus approfondi de la théorie de QAOA, y compris la dérivation complète du QUBO vers l'hamiltonien, consulte le module de cours QAOA.

Dans ce tutoriel, tu vas d'abord résoudre max-cut sur un petit graphe de cinq nœuds, puis faire passer le même flux de travail à un problème à l'échelle utilitaire de 100 nœuds sur du matériel réel. Remarque sur l'accès au plan : Ce tutoriel utilise les sessions Qiskit Runtime, qui ne sont disponibles que sur le plan Premium. Si tu es sur le plan Open, tu ne peux pas exécuter ce tutoriel tel quel ; à la place, tu devras remplacer Session par le mode job (c'est-à-dire soumettre chaque itération comme un job indépendant au lieu d'envelopper la boucle d'optimisation dans with Session(...)). Le flux de travail fonctionne quand même, mais chaque itération subit la totalité de la latence de file d'attente au lieu de réutiliser un dispositif réservé. Consulte Aperçu des plans disponibles pour plus d'informations.

Configuration requise

Avant de commencer ce tutoriel, assure-toi d'avoir installé les éléments suivants :

  • Qiskit SDK v2.0 ou version ultérieure, avec le support de visualisation
  • Qiskit Runtime v0.22 ou version ultérieure (pip install qiskit-ibm-runtime)

De plus, tu auras besoin d'un accès à une instance sur IBM Quantum® Platform.

Configuration

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime rustworkx scipy
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

Exemple à petite échelle

Cette section parcourt chaque étape du flux de travail QAOA sur une petite instance max-cut de cinq nœuds. Malgré l'étiquette « petite échelle », cet exemple s'exécute tout de même sur du matériel IBM Quantum réel — le code sélectionne un Backend avec 127 Qubits ou plus et y exécute le Circuit. Initialise ton problème en créant un graphe avec n=5n=5 nœuds.

n_small = 5

graph = rx.PyGraph()
graph.add_nodes_from(np.arange(0, n_small, 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)

Sortie de la cellule de code précédente

Étape 1 : Transposer les entrées classiques en un problème quantique

Transpose le graphe classique en circuits et opérateurs quantiques. Comme décrit dans le Contexte, pour le max-cut non pondéré l'hamiltonien de coût se réduit à HC=(i,j)EZiZjH_C = \sum_{(i,j) \in E} Z_i Z_j, et QAOA utilise un Circuit ansatz paramétré pour préparer des états fondamentaux candidats de HCH_C.

Construire l'hamiltonien de coût

Convertis les arêtes du graphe en termes de Pauli ZiZjZ_iZ_j pour construire HCH_C (voir le Contexte pour la dérivation).

def build_max_cut_paulis(
graph: rx.PyGraph,
) -> list[tuple[str, list[int], float]]:
"""Convert graph edges to a list of ZZ Pauli terms.

The returned list is in the sparse format expected by
``SparsePauliOp.from_sparse_list``: each element is
``(pauli_string, qubit_indices, coefficient)``.
"""
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_small)
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])

Construire le Circuit ansatz QAOA

Utilise QAOAAnsatz pour construire le Circuit QAOA paramétré à partir de l'hamiltonien de coût. Ici, nous utilisons reps=2 (deux couches QAOA, quatre paramètres : β0,β1,γ0,γ1\beta_0, \beta_1, \gamma_0, \gamma_1).

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

circuit.draw("mpl")

Sortie de la cellule de code précédente

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

Étape 2 : Optimiser le problème pour l'exécution sur du matériel quantique

Transpile le Circuit abstrait en instructions natives du matériel. Cette étape gère le mappage des Qubits, la décomposition des Gates, le routage et la suppression d'erreurs. Consulte la documentation sur la transpilation pour plus d'informations.

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

# Create pass manager for transpilation. Level 3 is the most aggressive
# preset: slower to transpile, but produces shorter circuits that are
# more robust to hardware noise.
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('ibm_pittsburgh')>

Sortie de la cellule de code précédente

Étape 3 : Exécuter à l'aide des primitives Qiskit

La boucle d'optimisation QAOA s'exécute au sein d'une session Qiskit Runtime pour maintenir le dispositif réservé d'une itération à l'autre. Un Estimator évalue HC\langle H_C \rangle à chaque étape, et un optimiseur classique (COBYLA) met à jour les paramètres jusqu'à convergence.

Illustration montrant le comportement des modes d&#39;exécution Job unique, Batch et Session. Définis les paramètres initiaux et exécute la boucle d'optimisation :

# QAOA doesn't prescribe principled default angles — any bounded choice
# works as a warm start for problems this small. beta and gamma are
# periodic (beta in [0, pi] and gamma in [0, 2*pi] modulo the underlying
# Pauli-rotation periods), and pi/2 and pi are just midpoints of those
# ranges. For harder problems you would typically warm start from known
# good angles or transfer parameters from smaller instances.
initial_gamma = np.pi
initial_beta = np.pi / 2
init_params = [initial_beta, initial_beta, initial_gamma, initial_gamma]
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"
estimator.options.environment.job_tags = ["TUT_QAOA"]

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: -2.0402211719947774
x: [ 3.041e+00 1.212e+00 2.081e+00 4.471e+00]
nfev: 36
maxcv: 0.0

L'optimiseur a pu réduire le coût et trouver de meilleurs paramètres pour le Circuit.

Une courbe décroissant de manière régulière qui plafonne est la signature de la convergence. Une courbe bruitée et non monotone indique généralement qu'un élément en amont nécessite de l'attention ; les causes courantes sont un nombre de shots trop faible par évaluation (variance élevée de l'estimateur), de mauvais paramètres initiaux, ou un Circuit dont la profondeur est dominée par le bruit du matériel. COBYLA est sans dérivée et assez robuste à un bruit modéré, mais lorsque le bruit submerge les améliorations réelles du coût à chaque étape, son modèle d'approximation linéaire ne peut plus distinguer une véritable descente d'une fluctuation aléatoire et l'optimiseur dérive.

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

Sortie de la cellule de code précédente

Assigne les paramètres optimisés et échantillonne la distribution finale à l'aide de la primitive Sampler.

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

Sortie de la cellule de code précédente

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

sampler.options.environment.job_tags = ["TUT_QAOA"]

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)
{18: 0.039, 5: 0.0665, 20: 0.0973, 29: 0.0063, 9: 0.0899, 13: 0.0379, 2: 0.0047, 1: 0.0153, 11: 0.0932, 14: 0.0327, 12: 0.0314, 25: 0.0193, 21: 0.0398, 6: 0.0224, 4: 0.0197, 10: 0.0387, 3: 0.0181, 26: 0.07, 17: 0.0327, 19: 0.0332, 22: 0.0914, 24: 0.007, 0: 0.0033, 8: 0.0066, 30: 0.0158, 28: 0.0169, 27: 0.0222, 16: 0.0073, 7: 0.0057, 23: 0.0062, 15: 0.0054, 31: 0.0041}

Étape 4 : Post-traiter et retourner le résultat dans le format classique souhaité

Extrais la chaîne de bits la plus probable de la distribution échantillonnée. Elle représente la meilleure coupe trouvée par QAOA.

# 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, 0, 1, 0, 1]
plt.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()

Sortie de la cellule de code précédente

Visualiser la meilleure coupe

À partir de la chaîne de bits optimale, tu peux ensuite visualiser cette coupe sur le graphe original.

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

Sortie de la cellule de code précédente

Maintenant, calcule la valeur de la coupe :

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

Pour un graphe aussi petit, le véritable optimum est facile à trouver par force brute, tu peux donc revérifier les résultats en comparant le résultat de QAOA avec la réponse exacte.

# Classical baseline: enumerate all 2**n_small bitstrings and take the best cut.
def brute_force_max_cut(graph: rx.PyGraph) -> tuple[int, list[int]]:
n = len(list(graph.nodes()))
best_cut = -1
best_x: list[int] = []
for i in range(2**n):
x = [(i >> k) & 1 for k in range(n)]
cut = evaluate_sample(x, graph)
if cut > best_cut:
best_cut = int(cut)
best_x = x
return best_cut, best_x

classical_best, classical_x = brute_force_max_cut(graph)
print(f"Classical optimum (brute force): {classical_best}")
print(f"QAOA cut value: {cut_value}")
Classical optimum (brute force): 5
QAOA cut value: 5

Exemple matériel à grande échelle

Tu as accès à de nombreux dispositifs de plus de 100 Qubits sur IBM Quantum Platform. Sélectionnes-en un sur lequel résoudre max-cut sur un graphe pondéré de 100 nœuds. Il s'agit d'un problème à « l'échelle utilitaire ». Le flux de travail suit les mêmes étapes que ci-dessus, appliquées à un graphe beaucoup plus grand.

Flux de travail de bout en bout à l'échelle utilitaire

Les quatre étapes sont présentées ci-dessous, appliquées au graphe de 100 nœuds. La structure est la même que dans la présentation à petite échelle : transposer, transpiler, exécuter, post-traiter — mais avec un problème plus grand et réparti sur les quatre cellules ci-dessous par souci de clarté.

# Precomputed parity lookup table: _PARITY[b] = +1 if popcount(b) is even, else -1.
# We use this to vectorize expectation-value evaluation across all Pauli terms.
_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:
"""Expectation value of a SparsePauliOp on a single computational-basis state.

For a Z-only observable (which QAOA cost Hamiltonians are, after the
QUBO-to-Hamiltonian mapping), the eigenvalue of each Pauli term on a
computational-basis state is simply (-1)**popcount(z_mask AND state),
i.e., the parity of the bitwise-AND of the term's Z-support and the
measured bitstring.

This routine packs the Z-support of every Pauli term into bytes, ANDs
them against the measured state in a single vectorized op, and looks up
the parity in _PARITY. For a 100-qubit / ~hundreds-of-terms Hamiltonian
over 10_000 samples, this is dramatically faster than calling
SparsePauliOp.expectation_value per sample.
"""
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):
"""Return the sampled bitstring (as int) with the lowest Hamiltonian cost."""
min_cost = float("inf")
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_cost = fval
min_sol = candidate_sol
return min_sol

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):
"""Convert the samples to values of the objective function."""
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

Étape 1 : Construis le graphe, l'hamiltonien de coût et l'ansatz.

# Step 1: build the 100-node graph, cost Hamiltonian, and QAOA ansatz.
n_large = 100
graph_100 = rx.PyGraph()
graph_100.add_nodes_from(np.arange(0, n_large, 1))
elist = []
for edge in backend.coupling_map:
if edge[0] < n_large and edge[1] < n_large:
elist.append((edge[0], edge[1], 1.0))
graph_100.add_edges_from(elist)

max_cut_paulis_100 = build_max_cut_paulis(graph_100)
cost_hamiltonian_100 = SparsePauliOp.from_sparse_list(
max_cut_paulis_100, n_large
)

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

Étape 2 : Transpile pour le Backend matériel sélectionné.

# Step 2: transpile for hardware.
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
candidate_circuit_100 = pm.run(circuit_100)

Étape 3 : Exécute la boucle d'optimisation QAOA au sein d'une session, puis échantillonne.

# Step 3: run the QAOA optimization loop on the device, then sample the
# final distribution with the optimized parameters.
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

# 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"
estimator.options.environment.job_tags = ["TUT_QAOA"]

result = minimize(
cost_func_estimator,
init_params,
args=(candidate_circuit_100, cost_hamiltonian_100, estimator),
method="COBYLA",
)
print(result)

# Assign optimal parameters and sample the final distribution.
optimized_circuit_100 = candidate_circuit_100.assign_parameters(result.x)

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"

# Add a unique tag to the job execution
sampler.options.environment.job_tags = ["TUT_QAOA"]

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

counts_int = job.result()[0].data.meas.get_int_counts()
shots = sum(counts_int.values())
final_distribution_100_int = {
key: val / shots for key, val in counts_int.items()
}
message: Return from COBYLA because the trust region radius reaches its lower bound.
success: True
status: 0
fun: -17.172689238986344
x: [ 2.574e+00 4.166e+00]
nfev: 28
maxcv: 0.0

Étape 4 : Post-traite la distribution échantillonnée pour extraire la meilleure coupe.

# Step 4: find the best-cost sample and evaluate its cut value.
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)

cut_value_100 = evaluate_sample(best_sol_bitstring_100, graph_100)
print("The value of the cut is:", cut_value_100)
Result bitstring: [1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0]
The value of the cut is: 156

Vérifie que le coût minimisé dans la boucle d'optimisation a convergé, et visualise les résultats.

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

# Visualize the cut
plot_result(graph_100, best_sol_bitstring_100)

# Plot cumulative distribution function
result_dist = samples_to_objective_values(
final_distribution_100_int, cost_hamiltonian_100
)
fig, ax = plt.subplots(1, 1, figsize=(8, 6))
plot_cdf(result_dist, ax, backend.name)

Sortie de la cellule de code précédente

Sortie de la cellule de code précédente

Sortie de la cellule de code précédente

Étapes suivantes

Si ce travail t'a intéressé, le matériel suivant pourrait t'intéresser :

Recommandations