Skip to main content
IBM Quantum Platform

Algorithme d'optimisation approximative quantique

Estimation de l'utilisation : 22 minutes sur un processeur Heron r3 (REMARQUE : il s'agit uniquement d'une estimation). Votre durée d'exécution peut varier.)


Résultats d'apprentissage

À l'issue de ce tutoriel, vous devriez être en mesure de comprendre les points suivants :

  • Comment transposer un problème classique d'optimisation combinatoire (coupe maximale) en un hamiltonien quantique
  • Comment implémenter et exécuter l'algorithme d'optimisation quantique approximative (QAOA) à l'aide de sessions du service de calcul d' IBM Quantum
  • Comment faire évoluer un workflow QAOA, depuis un exemple sur petit simulateur jusqu'à une exécution sur du matériel à l'échelle industrielle

Prérequis

Nous vous recommandons de vous familiariser avec les sujets suivants :


Arrière-plan

L 'algorithme d'optimisation approximative quantique (QAOA) est une méthode itérative hybride quantique-classique permettant de résoudre des problèmes d'optimisation combinatoire. Dans ce tutoriel, vous utiliserez QAOA pour résoudre le problème de la coupe maximale (max-cut) — un problème d'optimisation NP-difficile qui trouve des applications dans le regroupement de données, la science des réseaux et la physique statistique. Étant donné un graphe composé de nœuds relié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

Le problème de Max-cut peut être formulé comme un problème d'optimisation binaire classique. À chaque nœud est attribuée 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 appartiennent à 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.

Cela revient à un problème d'optimisation binaire quadratique sans contrainte (QUBO) de la forme minxxTQx\min_x\, x^T Q x. Grâce à une substitution de variables standard ( xi(1Zi)/2x_i \to (1 - Z_i)/2 ), le problème QUBO peut être réécrit sous la forme d'un hamiltonien de coût dont l'état fondamental code 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 de coupe maximale non pondérée considéré ici, les coefficients linéaires s'annulent ( bi=0b_i = 0 ) et Qij=1Q_{ij} = 1 pour chaque arête, ce qui donne la forme simplifiée HC=(i,j)EZiZjH_C = \sum_{(i,j) \in E} Z_i Z_j que vous allez implémenter dans le code ci-dessous. La forme plus générale ci-dessus est celle dont vous aurez besoin pour adapter ce workflow aux graphes pondérés ou à d'autres problèmes pouvant être formulés sous forme de QUBO.

Comment fonctionne le QAOA

La méthode QAOA élabore 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 de calcul quantique, ce qui permet de maintenir le dispositif quantique réservé d'une itération à l'autre afin de réduire la latence.

Schéma de circuit avec couches QAOA

Pour une étude plus approfondie de la théorie QAOA, y compris la déduction complète du modèle QUBO vers l'hamiltonien, consultez le module de cours sur la QAOA.

Dans ce tutoriel, vous allez d'abord résoudre le problème du « max-cut » sur un petit graphe à cinq nœuds, puis adapter ce même processus à un problème à grande échelle portant sur 100 nœuds, sur du matériel réel. Remarque concernant l'accès aux formules : ce tutoriel utilise des sessions Quantum Compute, qui ne sont disponibles que dans la formule Premium. Si vous utilisez le plan « Open Plan », vous ne pouvez pas suivre ce tutoriel tel quel; vous devrez plutôt passer en Session mode « job » (c'est-à-dire soumettre chaque itération sous forme de tâche indépendante plutôt que d'encapsuler la boucle d'optimisation dans Session(...)). Le workflow continue de s'exécuter, mais chaque itération subit la latence totale de la file d'attente au lieu de réutiliser un périphérique réservé. Pour plus d'informations, consultez la rubrique « Présentation des formules disponibles ».


Exigences

Avant de commencer ce tutoriel, assurez-vous que les éléments suivants sont installés :

  • Qiskit SDK v2.0 ou version ultérieure, avec prise en charge de la visualisation
  • Qiskit Runtime v0.22 ou plus tard (pip install qiskit-ibm-runtime)

De plus, vous devrez disposer d'un accès à une instance sur IBM Quantum® Platform.


Configuration

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 présente étape par étape le déroulement du processus QAOA sur une petite instance de problème de coupe maximale à cinq nœuds. Même s'il est qualifié de « petite échelle », cet exemple s'exécute tout de même sur du matériel d' IBM Quantum s réel : le code sélectionne un backend comportant au moins 127 qubits et y exécute le circuit.

Initialisez votre 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)

Output:

Output of the previous code cell

Étape 1 : Mettre en correspondance les entrées classiques avec un problème quantique

Représentez le graphe classique sous forme de circuits quantiques et d'opérateurs. Comme indiqué dans la section « Contexte », pour le problème du « 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 la méthode QAOA utilise un circuit d'ansatz paramétré pour préparer des états fondamentaux candidats de HCH_C.

Construire l'hamiltonien des coûts

Convertissez les arêtes du graphe en termes Pauli ZiZjZ_iZ_j pour construire HCH_C (voir 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)

Output:

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 de l'approche QAOA

Utilisez QAOAAnsatz pour construire le circuit QAOA paramétré à partir de l'hamiltonien de coût. Nous utilisons reps=2 ici (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")

Output:

Output of the previous code cell
circuit.parameters

Output:

ParameterView([ParameterVectorElement(β[0]), ParameterVectorElement(β[1]), ParameterVectorElement(γ[0]), ParameterVectorElement(γ[1])])

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

Convertir le circuit abstrait en instructions natives du matériel. Cette étape gère le mappage des qubits, la décomposition des portes, le routage et la suppression des erreurs. Pour plus d'informations, consultez la documentation sur la transpilation.

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)

Output:

<IBMBackend('ibm_pittsburgh')>
Output of the previous code cell

Étape 3 : Exécutez à l'aide d' Qiskit primitives

La boucle d'optimisation QAOA s'exécute au sein d'une session Quantum Compute afin de garantir que le périphérique reste réservé d'une itération à l'autre. Un estimateur évalue l' HC\langle H_C \rangle e à chaque étape, et un optimiseur classique (COBYLA) met à jour les paramètres jusqu'à convergence.

Illustration montrant le comportement des modes d'exécution Single job, Batch et Session.

Définissez les paramètres initiaux et lancez 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)

Output:

 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 permis de réduire le coût et de trouver de meilleurs paramètres pour le circuit.

Une courbe qui diminue progressivement avant de se stabiliser est le signe d'une convergence. Une courbe bruyante et non monotone indique généralement qu'il faut se pencher sur un problème en amont; les causes courantes sont un nombre insuffisant d'itérations par évaluation (variance élevée de l'estimateur), des paramètres initiaux inadéquats ou un circuit dont la profondeur est dominée par le bruit matériel. COBYLA ne fait appel à aucun dérivé et résiste assez bien à un bruit modéré, mais lorsque le bruit l'emporte sur les gains réels de coût par itération, son modèle d'approximation linéaire ne parvient plus à distinguer la descente réelle des fluctuations aléatoires, et l'optimiseur s'égare.

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

Output:

Output of the previous code cell

Attribuez les paramètres optimisés et échantillonnez 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)

Output:

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

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

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)

Output:

{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-traitement et restitution du résultat dans le format classique souhaité

Extraire la chaîne binaire la plus probable de la distribution échantillonnée. Il s'agit de la meilleure version 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)

Output:

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

Output:

Output of the previous code cell

Visualiser la meilleure coupe

À partir de la chaîne binaire optimale, vous pouvez ensuite visualiser cette découpe sur le graphe d'origine.

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


plot_result(graph, most_likely_bitstring)

Output:

Output of the previous code cell

Calculez maintenant la valeur de la dé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)

Output:

The value of the cut is: 5

Pour un graphe aussi petit, il est facile de trouver le véritable optimum par force brute; vous pouvez donc vérifier les résultats en comparant le résultat obtenu par l'algorithme QAOA à 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}")

Output:

Classical optimum (brute force): 5
QAOA cut value:                  5

Exemple de matériel à grande échelle

Sur IBM Quantum Platform, vous avez accès à de nombreux appareils dotés de plus de 100 qubits. Choisissez-en un sur lequel résoudre le problème du « max-cut » sur un graphe pondéré de 100 nœuds. Il s'agit d'un problème à « grande échelle ». Le processus suit les mêmes étapes que celles décrites ci-dessus, mais s'applique à un graphe beaucoup plus vaste.

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

Les quatre étapes sont présentées ci-dessous, appliquées au graphe de 100 nœuds. La structure est la même que celle de la démonstration à petite échelle : création de la carte, compilation, exécution, post-traitement — mais avec un problème plus complexe, réparti entre les quatre cellules ci-dessous pour plus 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 : Construire 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 : Transpiler 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écutez la boucle d'optimisation QAOA au sein d'une session, puis effectuez un échantillonnage.

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

Output:

 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 : Traiter la distribution échantillonnée afin d'en extraire la meilleure coupure.

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

Output:

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érifiez que le coût minimisé dans la boucle d'optimisation a convergé, puis visualisez 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)

Output:

Output of the previous code cell Output of the previous code cell Output of the previous code cell

Etapes suivantes

Si ce travail vous a paru intéressant, les ressources suivantes pourraient vous intéresser :

Recommandations
Cette page a-t-elle été utile ?
Signaler un bogue, une coquille ou proposer du contenu sur GitHub.