Skip to main content
IBM Quantum Platform

Algoritmo cuántico de optimización aproximada

Estimación de uso: 22 minutos en un procesador Heron r3 (NOTA: Esto es sólo una estimación. Su tiempo de ejecución puede variar)


Resultados del aprendizaje

Una vez completado este tutorial, habrás adquirido los siguientes conocimientos:

  • Cómo mapear un problema clásico de optimización combinatoria (corte máximo) a un hamiltoniano cuántico
  • Cómo implementar y ejecutar el algoritmo de optimización cuántica aproximada (QAOA) mediante sesiones de Qiskit Runtime
  • Cómo ampliar un flujo de trabajo QAOA desde un ejemplo de simulador a pequeña escala hasta la ejecución en hardware a escala industrial

Requisitos previos

Se recomienda que te familiarices con estos temas:


En segundo plano

El algoritmo de optimización cuántica aproximada (QAOA) es un método iterativo híbrido cuántico-clásico para resolver problemas de optimización combinatoria. En este tutorial, utilizarás QAOA para resolver el problema del corte máximo (max-cut), un problema de optimización NP-difícil con aplicaciones en la agrupación de datos, la ciencia de redes y la física estadística. Dado un grafo formado por nodos conectados por aristas, el objetivo es dividir los nodos en dos conjuntos de tal manera que se maximice el número de aristas que cruzan la división.

Ilustración de un problema de corte máximo

De la optimización clásica a los circuitos cuánticos

Max-cut puede plantearse como un problema clásico de optimización binaria. A cada nodo se le asigna una variable binaria xi{0,1}x_i \in \{0, 1\} que indica a qué conjunto pertenece. El objetivo es maximizar el número de aristas cuyos extremos pertenezcan a conjuntos diferentes:

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

Esto equivale a un problema de optimización binaria cuadrática sin restricciones (QUBO) de la forma minxxTQx\min_x\, x^T Q x. Mediante una sustitución de variables estándar ( xi(1Zi)/2x_i \to (1 - Z_i)/2 ), el QUBO puede reescribirse como un hamiltoniano de coste cuyo estado fundamental codifica la solución óptima. En general, este hamiltoniano contiene tanto términos cuadráticos como lineales:

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

En el problema del corte máximo no ponderado que nos ocupa, los coeficientes lineales se anulan ( bi=0b_i = 0 ) y Qij=1Q_{ij} = 1 para cada arista, lo que da lugar a la forma más simple HC=(i,j)EZiZjH_C = \sum_{(i,j) \in E} Z_i Z_j, que desarrollarás en el código a continuación. La forma más general anterior es la que necesitarías para adaptar este flujo de trabajo a grafos ponderados u otros problemas expresables en QUBO.

Cómo funciona QAOA

El método QAOA elabora soluciones candidatas aplicando capas alternas de dos operadores a un estado de superposición inicial Hn0H^{\otimes n}|0\rangle : el operador de coste eiγkHCe^{-i\gamma_k H_C} y un operador mezclador eiβkHme^{-i\beta_k H_m}. Los ángulos γk\gamma_k y βk\beta_k se optimizan en un bucle de retroalimentación clásico; el ordenador cuántico evalúa la función de coste y un optimizador clásico actualiza los parámetros hasta alcanzar la convergencia. Este bucle iterativo se ejecuta dentro de una sesión de « Qiskit Runtime », lo que permite mantener el dispositivo cuántico reservado a lo largo de todas las iteraciones para reducir la latencia.

Diagrama de circuito con capas QAOA

Para un análisis más detallado de la teoría de la QAOA, incluida la derivación completa del QUBO al hamiltoniano, consulte el módulo del curso sobre QAOA.

En este tutorial, primero resolverás el problema del corte máximo en un pequeño grafo de cinco nodos y, a continuación, ampliarás ese mismo flujo de trabajo a un problema a escala industrial de 100 nodos en hardware real. Nota sobre el acceso al plan: Este tutorial utiliza sesiones de « Qiskit Runtime », que solo están disponibles en el plan Premium. Si estás en el plan Open, no podrás seguir este tutorial tal y como está escrito; en su lugar, tendrás que cambiar Session al modo de trabajo (es decir, enviar cada iteración como un trabajo independiente en lugar de envolver el bucle de optimización con Session(...)). El flujo de trabajo sigue ejecutándose, pero cada iteración sufre toda la latencia de la cola, en lugar de reutilizar un dispositivo reservado. Consulte la descripción general de los planes disponibles para obtener más información.


Requisitos

Antes de empezar este tutorial, asegúrate de tener instalado lo siguiente:

  • Qiskit SDK v2.0 o posterior, con soporte para visualización
  • Qiskit Runtime v0.22 o posterior (pip install qiskit-ibm-runtime)

Además, necesitarás tener acceso a una instancia en IBM Quantum® Platform.


Configuración

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

Ejemplo a pequeña escala

En esta sección se describe paso a paso el flujo de trabajo de QAOA utilizando una pequeña instancia de max-cut de cinco nodos. A pesar de la etiqueta de «pequeña escala», este ejemplo se ejecuta en un hardware real de IBM Quantum : el código selecciona un backend con 127 o más qubits y ejecuta el circuito en él.

Inicialice su problema creando un grafo con n=5n=5 nodos.

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

Paso 1: Asignar entradas clásicas a un problema cuántico

Representa el grafo clásico en circuitos y operadores cuánticos. Tal y como se describe en la sección «Antecedentes», para el problema del corte máximo no ponderado, el hamiltoniano de coste se reduce a HC=(i,j)EZiZjH_C = \sum_{(i,j) \in E} Z_i Z_j, y el QAOA utiliza un circuito de ansatz parametrizado para preparar estados fundamentales candidatos de HCH_C.

Calcular el hamiltoniano de costes

Convertir los arcos del grafo en términos de Pauli- ZiZjZ_iZ_j o para construir un grafo de tipo « HCH_C » (véase la sección «Antecedentes» para consultar la derivación).

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

Construir el circuito del modelo QAOA

Utiliza QAOAAnsatz para construir el circuito QAOA parametrizado a partir del hamiltoniano de coste. Aquí utilizamos reps=2 (dos capas QAOA, cuatro parámetros: β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])])

Paso 2: Optimizar el problema para la ejecución en hardware cuántico

Convertir el circuito abstracto en instrucciones nativas del hardware. Este paso se encarga de la asignación de qubits, la descomposición de puertas, el enrutamiento y la supresión de errores. Consulta la documentación sobre la transpilación para obtener más información.

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

Paso 3: Ejecutar utilizando Qiskit primitives

El bucle de optimización de QAOA se ejecuta dentro de una sesión de « Qiskit Runtime » para mantener el dispositivo reservado a lo largo de las iteraciones. Un estimador evalúa un HC\langle H_C \rangle e en cada paso, y un optimizador clásico (COBYLA) actualiza los parámetros hasta alcanzar la convergencia.

Ilustración que muestra el comportamiento de los modos de ejecución Trabajo único, Lote y Sesión.

Define los parámetros iniciales y ejecuta el ciclo de optimización:

# 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

El optimizador consiguió reducir el coste y encontrar mejores parámetros para el circuito.

Una curva que desciende suavemente y luego se estabiliza es el indicio de la convergencia. Una curva ruidosa y no monótona suele indicar que hay algún problema en las etapas anteriores que requiere atención; las causas más comunes son un número insuficiente de iteraciones por evaluación (alta varianza del estimador), parámetros iniciales inadecuados o un circuito cuya profundidad está dominada por el ruido del hardware. COBYLA no utiliza derivadas y es bastante resistente al ruido moderado, pero cuando el ruido supera las mejoras reales en el coste por paso, su modelo de aproximación lineal ya no puede distinguir el descenso real de las fluctuaciones aleatorias y el optimizador se desvía.

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

Asigna los parámetros optimizados y genera una muestra de la distribución final utilizando la primitiva «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}

Paso 4: Procesamiento posterior y devolución del resultado en el formato clásico deseado

Extrae la cadena de bits más probable de la distribución muestreada. Este es el mejor corte que ha encontrado 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

Visualizar el mejor corte

A partir de la cadena de bits óptima, puedes visualizar este corte en el gráfico 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)

Output:

Output of the previous code cell

Ahora, calcula el valor del corte:

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

En un grafo tan pequeño, es fácil hallar el óptimo verdadero mediante un método de fuerza bruta, por lo que puedes verificar los resultados comparando el resultado del algoritmo QAOA con la respuesta exacta.

# 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

Ejemplo de hardware a gran escala

En IBM Quantum Platform tienes acceso a numerosos dispositivos con más de 100 qubits. Elige uno en el que resolver el problema del corte máximo en un grafo ponderado de 100 nodos. Se trata de un problema a «escala industrial». El flujo de trabajo sigue los mismos pasos que los anteriores, pero aplicados a un gráfico mucho más grande.

Flujo de trabajo integral a escala industrial

A continuación se muestran los cuatro pasos, aplicados al gráfico de 100 nodos. La estructura es la misma que la del ejemplo a pequeña escala: mapa, transpilación, ejecución y posprocesamiento, pero con un problema más complejo y dividido entre las cuatro celdas siguientes para mayor claridad.

# 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

Paso 1 : Construir el grafo, el hamiltoniano de costes y el 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()

Paso 2 : Compilar para el backend de hardware seleccionado.

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

Paso 3 : Ejecuta el bucle de optimización de QAOA dentro de una sesión y, a continuación, toma una muestra.

# 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

Paso 4 : Procesar posteriormente la distribución muestreada para extraer el mejor corte.

# 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

Comprueba que el coste minimizado en el bucle de optimización haya convergido y visualiza los resultados.

# 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

Próximos pasos

Si te ha parecido interesante este trabajo, quizá te interese el siguiente material:

Recomendaciones
¿Le ha resultado útil esta página?
Informe de un error, de una errata o solicite contenido en GitHub.