Saltar al contenido principal

Algoritmo de optimización aproximada cuántica

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

Resultados de aprendizaje

Después de completar este tutorial, puedes esperar comprender la siguiente información:

  • Cómo mapear un problema clásico de optimización combinatoria (max-cut) a un Hamiltoniano cuántico
  • Cómo implementar y ejecutar el algoritmo de optimización aproximada cuántica (QAOA) usando sesiones de Qiskit Runtime
  • Cómo escalar un flujo de trabajo de QAOA desde un pequeño ejemplo en simulador hasta la ejecución en hardware a escala de utilidad

Prerrequisitos

Se recomienda que te familiarices con estos temas:

Contexto

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

Ilustración de un problema de corte máximo

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

Max-cut se puede expresar 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 están en 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 es equivalente 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 costo cuyo estado fundamental codifica la solución óptima. En general, este Hamiltoniano tiene términos tanto cuadráticos como lineales:

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

Para el problema de max-cut no ponderado considerado aquí, los coeficientes lineales se anulan (bi=0b_i = 0) y Qij=1Q_{ij} = 1 para cada arista, dejando la forma más simple HC=(i,j)EZiZjH_C = \sum_{(i,j) \in E} Z_i Z_j que construirás en el código más abajo. La forma más general anterior es lo que necesitarías para adaptar este flujo de trabajo a grafos ponderados u otros problemas expresables como QUBO.

Cómo funciona QAOA

QAOA prepara soluciones candidatas aplicando capas alternantes de dos operadores a un estado inicial de superposición Hn0H^{\otimes n}|0\rangle: el operador de costo 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; la computadora cuántica evalúa la función de costo, y un optimizador clásico actualiza los parámetros hasta la convergencia. Este bucle iterativo se ejecuta dentro de una Session de Qiskit Runtime, que mantiene el dispositivo cuántico reservado a lo largo de las iteraciones para una menor latencia.

Diagrama de circuito con capas QAOA

Para un tratamiento más profundo de la teoría de QAOA, incluida la derivación completa de QUBO a Hamiltoniano, consulta el módulo del curso de QAOA.

En este tutorial primero resolverás max-cut en un grafo pequeño de cinco nodos, y luego escalarás el mismo flujo de trabajo a un problema de 100 nodos a escala de utilidad en hardware real. Nota sobre el acceso al plan: Este tutorial usa sesiones de Qiskit Runtime, que solo están disponibles en el Plan Premium. Si estás en el Plan Abierto, no puedes ejecutar este tutorial tal como está escrito; en su lugar, necesitarás cambiar Session por el modo de trabajo (es decir, enviar cada iteración como un trabajo independiente en lugar de envolver el bucle de optimización en with Session(...)). El flujo de trabajo sigue funcionando, pero cada iteración incurre en la latencia completa de la cola en lugar de reutilizar un dispositivo reservado. Consulta Resumen de los planes disponibles para obtener más información.

Requisitos

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

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

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

Configuración

# 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

Ejemplo a pequeña escala

Esta sección recorre cada paso del flujo de trabajo de QAOA sobre una pequeña instancia de max-cut de cinco nodos. A pesar de la etiqueta "pequeña escala", este ejemplo todavía se ejecuta en hardware real de IBM Quantum — el código selecciona un Backend con 127 o más qubits y ejecuta el Circuit ahí. Inicializa tu 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)

Salida de la celda de código anterior

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

Mapea el grafo clásico en circuitos y operadores cuánticos. Como se describe en el Contexto, para max-cut no ponderado el Hamiltoniano de costo se reduce a HC=(i,j)EZiZjH_C = \sum_{(i,j) \in E} Z_i Z_j, y QAOA usa un Circuit ansatz parametrizado para preparar estados fundamentales candidatos de HCH_C.

Construir el Hamiltoniano de costo

Convierte las aristas del grafo en términos de Pauli ZiZjZ_iZ_j para construir HCH_C (consulta el Contexto para 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)
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 Circuit ansatz de QAOA

Usa QAOAAnsatz para construir el Circuit parametrizado de QAOA a partir del Hamiltoniano de costo. Aquí usamos reps=2 (dos capas de 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")

Salida de la celda de código anterior

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

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

Transpila el Circuit abstracto a instrucciones nativas del hardware. Este paso maneja el mapeo de qubits, la descomposición de Gates, el enrutamiento y la supresión de errores. Consulta la documentación de 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)
<IBMBackend('ibm_pittsburgh')>

Salida de la celda de código anterior

Paso 3: Ejecutar usando primitivas de Qiskit

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

Ilustración que muestra el comportamiento de los modos de ejecución de trabajo único, lote y sesión. Define los parámetros iniciales y ejecuta el bucle 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)
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 logró reducir el costo y encontrar mejores parámetros para el circuito.

Una curva que decrece suavemente y se estabiliza es la firma de la convergencia. Una curva ruidosa y no monótona suele indicar que algo aguas arriba necesita atención; las causas comunes son muy pocos shots por evaluación (alta varianza del estimador), parámetros iniciales deficientes o un circuito cuya profundidad está dominada por el ruido del hardware. COBYLA no usa derivadas y es bastante robusto frente a ruido moderado, pero cuando el ruido supera las mejoras reales del costo por paso, su modelo de aproximación lineal ya no puede distinguir el descenso real de la fluctuación aleatoria y el optimizador divaga.

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

Salida de la celda de código anterior

Asigna los parámetros optimizados y muestrea la distribución final usando la primitiva Sampler.

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

Salida de la celda de código anterior

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

Paso 4: Posprocesar y devolver el resultado en el formato clásico deseado

Extrae la cadena de bits más probable de la distribución muestreada. Esta representa el mejor corte encontrado por 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()

Salida de la celda de código anterior

Visualizar el mejor corte

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

Salida de la celda de código anterior

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)
The value of the cut is: 5

Para un grafo tan pequeño, el óptimo verdadero es fácil de obtener por fuerza bruta, por lo que puedes verificar los resultados comparando el resultado de 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}")
Classical optimum (brute force): 5
QAOA cut value: 5

Ejemplo de hardware a gran escala

Tienes acceso a muchos dispositivos con más de 100 qubits en IBM Quantum Platform. Selecciona uno en el que resolver max-cut sobre un grafo ponderado de 100 nodos. Este es un problema a "escala de utilidad". El flujo de trabajo sigue los mismos pasos que arriba, aplicados a un grafo mucho más grande.

Flujo de trabajo de extremo a extremo a escala de utilidad

Los cuatro pasos se muestran a continuación, aplicados al grafo de 100 nodos. La estructura es la misma que en el recorrido a pequeña escala: mapear, transpilar, ejecutar, posprocesar — pero con un problema más grande y dividido en 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: Construye el grafo, el Hamiltoniano de costo 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: Transpila 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 Session y luego muestrea.

# 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

Paso 4: Posprocesa 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)
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 costo minimizado en el bucle de optimización ha 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)

Salida de la celda de código anterior

Salida de la celda de código anterior

Salida de la celda de código anterior

Próximos pasos

Si te resultó interesante este trabajo, podría interesarte el siguiente material:

Recomendaciones