Pular para o conteúdo principal

Algoritmo de otimização aproximada quântica

Estimativa de uso: 22 minutos em um processador Heron r3 (NOTA: Esta é apenas uma estimativa. Seu tempo de execução pode variar.)

Resultados de aprendizagem

Após concluir este tutorial, você pode esperar entender as seguintes informações:

  • Como mapear um problema clássico de otimização combinatória (max-cut) para um Hamiltoniano quântico
  • Como implementar e executar o Algoritmo de Otimização Aproximada Quântica (QAOA) usando sessions do Qiskit Runtime
  • Como escalar um fluxo de trabalho QAOA de um pequeno exemplo de simulador até a execução em hardware em escala de utilidade

Pré-requisitos

É recomendado que você se familiarize com estes tópicos:

Contexto

O Algoritmo de Otimização Aproximada Quântica (QAOA) é um método iterativo híbrido quântico-clássico para resolver problemas de otimização combinatória. Neste tutorial, você usará o QAOA para resolver o problema de corte máximo (max-cut) — um problema de otimização NP-difícil com aplicações em agrupamento, ciência de redes e física estatística. Dado um grafo de nós conectados por arestas, o objetivo é particionar os nós em dois conjuntos de modo que o número de arestas que atravessam a partição seja maximizado.

Ilustração de um problema de max-cut

Da otimização clássica aos circuits quânticos

O max-cut pode ser expresso como um problema clássico de otimização binária. A cada nó é atribuída uma variável binária xi{0,1}x_i \in \{0, 1\} que indica a qual conjunto ele pertence. O objetivo é maximizar o número de arestas em que os extremos estão em 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.

Isso é equivalentemente um problema de Otimização Binária Irrestrita Quadrática (QUBO) da forma minxxTQx\min_x\, x^T Q x. Por meio de uma substituição padrão de variáveis (xi(1Zi)/2x_i \to (1 - Z_i)/2), o QUBO pode ser reescrito como um Hamiltoniano de custo cujo estado fundamental codifica a solução ótima. Em geral, esse Hamiltoniano possui termos tanto quadráticos quanto lineares:

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

Para o problema de max-cut não ponderado considerado aqui, os coeficientes lineares se anulam (bi=0b_i = 0) e Qij=1Q_{ij} = 1 para cada aresta, restando a forma mais simples HC=(i,j)EZiZjH_C = \sum_{(i,j) \in E} Z_i Z_j que você construirá em código abaixo. A forma mais geral acima é o que você precisaria para adaptar este fluxo de trabalho a grafos ponderados ou a outros problemas expressáveis como QUBO.

Como o QAOA funciona

O QAOA prepara soluções candidatas aplicando camadas alternadas de dois operadores a um estado de superposição inicial Hn0H^{\otimes n}|0\rangle: o operador de custo eiγkHCe^{-i\gamma_k H_C} e um operador mixer eiβkHme^{-i\beta_k H_m}. Os ângulos γk\gamma_k e βk\beta_k são otimizados em um loop de feedback clássico; o computador quântico avalia a função de custo, e um otimizador clássico atualiza os parâmetros até a convergência. Esse loop iterativo é executado dentro de uma session do Qiskit Runtime, que mantém o dispositivo quântico reservado ao longo das iterações para menor latência.

Diagrama de circuit com camadas QAOA

Para um tratamento mais aprofundado da teoria do QAOA, incluindo a derivação completa de QUBO para Hamiltoniano, consulte o módulo do curso de QAOA.

Neste tutorial, você primeiro resolverá o max-cut em um pequeno grafo de cinco nós e, em seguida, escalará o mesmo fluxo de trabalho para um problema de 100 nós em escala de utilidade em hardware real. Nota sobre o acesso ao plano: Este tutorial usa sessions do Qiskit Runtime, que estão disponíveis apenas no Plano Premium. Se você estiver no Plano Aberto, não poderá executar este tutorial como está escrito; em vez disso, será necessário trocar Session pelo modo de job (ou seja, enviar cada iteração como um job independente em vez de envolver o loop de otimização com Session(...)). O fluxo de trabalho ainda funciona, mas cada iteração incorre na latência total da fila em vez de reutilizar um dispositivo reservado. Consulte Visão geral dos planos disponíveis para mais informações.

Requisitos

Antes de começar este tutorial, certifique-se de ter o seguinte instalado:

  • Qiskit SDK v2.0 ou posterior, com suporte para visualização
  • Qiskit Runtime v0.22 ou posterior (pip install qiskit-ibm-runtime)

Além disso, você precisará de acesso a uma instância na IBM Quantum® Platform.

Configuração

# 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

Exemplo em pequena escala

Esta seção percorre cada passo do fluxo de trabalho QAOA em uma pequena instância de max-cut de cinco nós. Apesar do rótulo "pequena escala", este exemplo ainda é executado em hardware real da IBM Quantum — o código seleciona um backend com 127 ou mais qubits e executa o circuit nele. Inicialize seu problema criando um grafo com n=5n=5 nós.

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)

Saída da célula de código anterior

Passo 1: Mapear entradas clássicas para um problema quântico

Mapeie o grafo clássico para circuits e operadores quânticos. Como descrito no Contexto, para o max-cut não ponderado o Hamiltoniano de custo se reduz a HC=(i,j)EZiZjH_C = \sum_{(i,j) \in E} Z_i Z_j, e o QAOA usa um circuit ansatz parametrizado para preparar estados fundamentais candidatos de HCH_C.

Construir o Hamiltoniano de custo

Converta as arestas do grafo em termos Pauli ZiZjZ_iZ_j para construir HCH_C (consulte o Contexto para a derivação).

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 o circuit ansatz QAOA

Use QAOAAnsatz para construir o circuit QAOA parametrizado a partir do Hamiltoniano de custo. Aqui usamos reps=2 (duas camadas QAOA, quatro 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")

Saída da célula de código anterior

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

Passo 2: Otimizar problema para execução em hardware quântico

Transpile o circuit abstrato em instruções nativas de hardware. Este passo lida com mapeamento de qubits, decomposição de gates, roteamento e supressão de erro. Consulte a documentação de transpilação para mais informações.

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

Saída da célula de código anterior

Passo 3: Executar usando primitivas Qiskit

O loop de otimização QAOA é executado dentro de uma session do Qiskit Runtime para manter o dispositivo reservado ao longo das iterações. Um Estimator avalia HC\langle H_C \rangle em cada passo, e um otimizador clássico (COBYLA) atualiza os parâmetros até a convergência.

Ilustração mostrando o comportamento dos modos de runtime Single job, Batch e Session. Defina os parâmetros iniciais e execute o loop de otimização:

# 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

O otimizador conseguiu reduzir o custo e encontrar melhores parâmetros para o circuit.

Uma curva que decresce suavemente e atinge um platô é a assinatura da convergência. Uma curva ruidosa e não monotônica geralmente indica que algo a montante precisa de atenção; causas comuns são poucos shots por avaliação (alta variância do estimator), parâmetros iniciais ruins ou um circuit cuja profundidade é dominada pelo ruído do hardware. O COBYLA é livre de derivadas e razoavelmente robusto a ruído moderado, mas quando o ruído supera as melhorias reais de custo por passo, seu modelo de aproximação linear não consegue mais distinguir a descida real da flutuação aleatória e o otimizador acaba vagando.

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

Saída da célula de código anterior

Atribua os parâmetros otimizados e amostre a distribuição final usando a primitiva Sampler.

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

Saída da célula 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}

Passo 4: Pós-processar e retornar o resultado no formato clássico desejado

Extraia a bitstring mais provável da distribuição amostrada. Ela representa o melhor corte encontrado pelo 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()

Saída da célula de código anterior

Visualizar o melhor corte

A partir da bitstring ótima, você pode então visualizar este corte no 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)

Saída da célula de código anterior

Agora, calcule o valor do 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 um grafo tão pequeno, o verdadeiro ótimo é fácil de obter por força bruta, então você pode verificar os resultados comparando o resultado do QAOA com a resposta exata.

# 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

Exemplo de hardware em larga escala

Você tem acesso a muitos dispositivos com mais de 100 qubits na IBM Quantum Platform. Selecione um no qual resolver o max-cut em um grafo ponderado de 100 nós. Este é um problema em "escala de utilidade". O fluxo de trabalho segue os mesmos passos acima, aplicados a um grafo muito maior.

Fluxo de trabalho de ponta a ponta em escala de utilidade

Os quatro passos são mostrados abaixo, aplicados ao grafo de 100 nós. A estrutura é a mesma do passo a passo em pequena escala: mapear, transpilar, executar, pós-processar — mas com um problema maior e dividido entre as quatro células abaixo para maior clareza.

# 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

Passo 1: Construa o grafo, o Hamiltoniano de custo e o 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()

Passo 2: Transpile para o backend de hardware selecionado.

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

Passo 3: Execute o loop de otimização QAOA dentro de uma session e, em seguida, amostre.

# 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

Passo 4: Pós-processe a distribuição amostrada para extrair o melhor 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

Verifique se o custo minimizado no loop de otimização convergiu e visualize os 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)

Saída da célula de código anterior

Saída da célula de código anterior

Saída da célula de código anterior

Próximos passos

Se você achou este trabalho interessante, talvez se interesse pelo seguinte material:

Recomendações