Vai al contenuto principale

Optimization Solver: una Qiskit Function di Q-CTRL Fire Opal

nota

Le Qiskit Functions sono una funzionalità sperimentale disponibile esclusivamente per gli utenti del piano IBM Quantum® Premium, del piano Flex e del piano On-Prem (tramite IBM Quantum Platform API). Sono in stato di anteprima e soggette a modifiche.

Panoramica​

Con il Fire Opal Optimization Solver puoi risolvere problemi di ottimizzazione su scala utility su hardware quantistico senza dover possedere competenze specifiche nel campo quantistico. Inserisci semplicemente la definizione del problema ad alto livello e il Solver si occupa del resto. L'intero flusso di lavoro è noise-aware e sfrutta internamente il Performance Management di Fire Opal. Il Solver fornisce in modo costante soluzioni accurate a problemi classicamente difficili, anche alla scala completa del dispositivo sui più grandi QPU IBM®.

Il Solver è flessibile e può essere utilizzato per risolvere problemi di ottimizzazione combinatoria definiti come funzioni obiettivo o come grafi arbitrari. I problemi non devono necessariamente essere mappati alla topologia del dispositivo. Sia i problemi non vincolati che quelli vincolati sono risolvibili, a condizione che i vincoli possano essere formulati come termini di penalità. Gli esempi inclusi in questa guida mostrano come risolvere un problema di ottimizzazione su scala utility, sia non vincolato che vincolato, utilizzando diversi tipi di input per il Solver. Il primo esempio riguarda un problema Max-Cut definito su un grafo 3-regolare a 156 nodi, mentre il secondo affronta un problema di Minimum Vertex Cover a 50 nodi definito da una funzione di costo.

Per ottenere l'accesso all'Optimization Solver, contatta Q-CTRL.

Descrizione della funzione​

Il Solver ottimizza e automatizza completamente l'intero algoritmo, dalla soppressione degli errori a livello hardware alla mappatura efficiente del problema e all'ottimizzazione classica a ciclo chiuso. Dietro le quinte, la pipeline del Solver riduce gli errori a ogni fase, abilitando le prestazioni avanzate necessarie per scalare in modo significativo. Il flusso di lavoro sottostante è ispirato al Quantum Approximate Optimization Algorithm (QAOA), un algoritmo ibrido quantistico-classico. Per un riepilogo dettagliato dell'intero flusso di lavoro dell'Optimization Solver, consulta il manoscritto pubblicato.

Visualizzazione del flusso di lavoro dell'Optimization Solver

Per risolvere un problema generico con l'Optimization Solver:

  1. Definisci il tuo problema come funzione obiettivo, come grafo o come catena di spin SparsePauliOp.
  2. Collegati alla funzione tramite il Qiskit Functions Catalog.
  3. Esegui il problema con il Solver e recupera i risultati.

Input e output​

Input​

NomeTipoDescrizioneObbligatorioPredefinitoEsempio
problemstr o SparsePauliOpUna delle rappresentazioni elencate in "Formati di problema accettati"SìN/APoly(2.0*n[0]*n[1] + 2.0*n[0]*n[2] - 3.13520241298341*n[0] + 2.0*n[1]*n[2] - 3.14469748506794*n[1] - 3.18897660121566*n[2] + 6.0, n[0], n[1], n[2], domain='RR')
problem_typestrNome della classe del problema; usato solo per le definizioni di problema basate su grafo e catena di spin, limitate a "maxcut" o "spin_chain"; non richiesto per definizioni di funzione obiettivo arbitrariaNoNone"maxcut"
backend_namestrNome del backend da utilizzareNoIl backend meno occupato a cui la tua istanza ha accesso"ibm_fez"
optionsdictOpzioni di input, tra cui: (Facoltativo) session_id: str; il comportamento predefinito crea una nuova SessionNoNone{"session_id": "cw2q70c79ws0008z6g4g"}

Formati di problema accettati:

  • Rappresentazione in espressione polinomiale di una funzione obiettivo. Idealmente creata in Python con un oggetto SymPy Poly esistente e formattata in stringa utilizzando sympy.srepr.
  • Rappresentazione tramite grafo di un tipo di problema specifico. Il grafo deve essere creato utilizzando la libreria networkx in Python e poi convertito in stringa tramite la funzione networkx [nx.readwrite.json_graph.adjacency_data](http://nx.readwrite.json_graph.adjacency_data.).
  • Rappresentazione in catena di spin di un problema specifico. La catena di spin deve essere rappresentata come oggetto SparsePauliOp; consulta la documentazione per maggiori dettagli.

Backend supportati: Esegui il codice seguente per visualizzare l'elenco dei backend attualmente supportati. Se il tuo dispositivo non è presente nell'elenco, contatta Q-CTRL per aggiungere il supporto.

# Added by doQumentation — required packages for this notebook
!pip install -q networkx numpy qiskit-ibm-catalog qiskit-ibm-runtime sympy
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService()

service.backends()
[<IBMBackend('ibm_fez')>,
<IBMBackend('ibm_brisbane')>,
<IBMBackend('ibm_pittsburgh')>,
<IBMBackend('ibm_kingston')>,
<IBMBackend('ibm_torino')>,
<IBMBackend('ibm_marrakesh')>]

Opzioni:

NomeTipoDescrizionePredefinito
session_idstrID di una sessione Qiskit Runtime esistente"cw4r3je6f0t010870y3g"
job_tagslist[str]Un elenco di tag per i job[]

Output​

NomeTipoDescrizioneEsempio
resultdict[str, Any]Soluzione e metadati elencati in "Contenuto del dizionario dei risultati"{'solution_bitstring_cost': 3.0, 'final_bitstring_distribution': {'000001': 100, '000011': 2}, 'iteration_count': 3, 'solution_bitstring': '000001', 'variables_to_bitstring_index_map': {n[1]': 5, 'n[2]': 4, 'n[3]': 3, 'n[4]': 2, 'n[5]': 1}, 'best_parameters': [0.19628831763697097, -1.047052334523102], 'warnings': []}

Contenuto del dizionario dei risultati:

NomeTipoDescrizione
solution_bitstring_costfloatIl miglior costo minimo tra tutte le iterazioni
final_bitstring_distributionCountsDictIl dizionario dei conteggi delle bitstring associato al costo minimo
solution_bitstringstrBitstring con il costo migliore nella distribuzione finale
iteration_countintIl numero totale di iterazioni QAOA eseguite dal Solver
variables_to_bitstring_index_mapfloatLa mappatura dalle variabili al bit equivalente nella bitstring
best_parameterslist[float]I parametri beta e gamma ottimizzati tra tutte le iterazioni
warningslist[str]Gli avvisi prodotti durante la compilazione o l'esecuzione del QAOA (predefinito: None)

Benchmark​

I risultati di benchmarking pubblicati mostrano che il Solver risolve con successo problemi con oltre 120 qubit, superando persino i risultati precedentemente pubblicati su dispositivi a quantum annealing e ad ioni intrappolati. Le seguenti metriche di benchmark forniscono un'indicazione approssimativa dell'accuratezza e della scalabilità dei tipi di problema sulla base di alcuni esempi. Le metriche effettive possono variare in base a diverse caratteristiche del problema, come il numero di termini nella funzione obiettivo (densità) e la loro località, il numero di variabili e l'ordine polinomiale.

Il "Numero di qubit" indicato non è un limite assoluto, ma rappresenta soglie approssimative entro le quali ci si può aspettare un'accuratezza della soluzione estremamente costante. Problemi di dimensioni maggiori sono stati risolti con successo e si incoraggia la sperimentazione oltre questi limiti.

La connettività arbitraria dei qubit è supportata per tutti i tipi di problema.

Tipo di problemaNumero di qubitEsempioAccuratezzaTempo totale (s)Utilizzo runtime (s)Numero di iterazioni
Problemi quadratici con connettività sparsa156Max-Cut 3-regolare100%176429316
Ottimizzazione binaria di ordine superiore156Modello di spin-glass di Ising100%146127216
Problemi quadratici con connettività densa50Max-Cut completamente connesso100%175826812
Problema vincolato con termini di penalità50Minimum Vertex Cover pesato con densità di archi 8%100%107421510

Inizia a usarlo​

Prima di tutto, autenticati usando la tua chiave API di IBM Quantum. Poi seleziona la Qiskit Function come segue. (Questo snippet presuppone che tu abbia già salvato il tuo account nell'ambiente locale.)

from qiskit_ibm_catalog import QiskitFunctionsCatalog

catalog = QiskitFunctionsCatalog(channel="ibm_quantum_platform")

# Access Function
solver = catalog.load("q-ctrl/optimization-solver")

Esempio: ottimizzazione non vincolata​

Esegui il problema del taglio massimo (Max-Cut). Il seguente esempio dimostra le capacità del Solver su un problema Max-Cut su un grafo 3-regolare non pesato a 156 nodi, ma puoi risolvere anche problemi su grafi pesati. Oltre a qiskit-ibm-catalog, per eseguire questo esempio avrai bisogno anche dei seguenti pacchetti: networkx e numpy. Puoi installare questi pacchetti decommentando la cella seguente se stai eseguendo questo esempio in un notebook con il kernel IPython.

# %pip install networkx numpy

1. Definisci il problema​

Puoi eseguire un problema Max-Cut definendo un problema su grafo e specificando problem_type='maxcut'.

import networkx as nx
import numpy as np

# Generate a random graph with 156 nodes
maxcut_graph = nx.random_regular_graph(d=3, n=156, seed=8)
# Optionally, visualize the graph
nx.draw_networkx(
maxcut_graph, nx.kamada_kawai_layout(maxcut_graph), node_size=100
)

Output della cella di codice precedente

Il Solver accetta una stringa come input per la definizione del problema.

# Convert graph to string
problem_as_str = nx.readwrite.json_graph.adjacency_data(maxcut_graph)

2. Esegui il problema​

Quando si utilizza il metodo di input basato su grafo, è necessario specificare il tipo di problema.

# This cell is hidden from users
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService()
backend_name = service.least_busy(n_qubits=156).name
# Solve the problem
maxcut_job = solver.run(
problem=problem_as_str,
problem_type="maxcut",
backend_name=backend_name, # E.g. "ibm_fez"
)

Controlla lo stato del workload della tua Qiskit Function o recupera i risultati come segue:

# Get job status
print(maxcut_job.status())
QUEUED

3. Recupera il risultato​

Recupera il valore del taglio ottimale dal dizionario dei risultati.

nota
La mappatura delle variabili alla bitstring potrebbe essere cambiata. Il dizionario di output contiene un sotto-dizionario variables_to_bitstring_index_map che aiuta a verificare l'ordinamento.
# Poll for results
maxcut_result = maxcut_job.result()

# Take the absolute value of the solution since the cost function is minimized
qctrl_maxcut = abs(maxcut_result["solution_bitstring_cost"])

# Print the optimal cut value found by the Optimization Solver
print(f"Optimal cut value: {qctrl_maxcut}")
Optimal cut value: 209.0

Puoi verificare l'accuratezza del risultato risolvendo il problema classicamente con solver open-source come PuLP se il grafo non è densamente connesso. I problemi ad alta densità potrebbero richiedere solver classici avanzati per validare la soluzione.

Esempio: ottimizzazione vincolata​

Il precedente esempio Max-Cut è un classico problema di ottimizzazione binaria quadratica non vincolata. L'Optimization Solver di Q-CTRL può essere utilizzato per vari tipi di problemi, inclusa l'ottimizzazione vincolata. Puoi risolvere tipi di problemi arbitrari inserendo la definizione del problema rappresentata come polinomio, dove i vincoli sono modellati come termini di penalità.

Il seguente esempio mostra come costruire una funzione di costo per un problema di ottimizzazione vincolata, il minimum vertex cover (MVC). Oltre ai pacchetti qiskit-ibm-catalog e qiskit, per eseguire questo esempio avrai bisogno anche dei seguenti pacchetti: numpy, networkx e sympy. Puoi installare questi pacchetti decommentando la cella seguente se stai eseguendo questo esempio in un notebook con il kernel IPython.

# %pip install numpy networkx sympy

1. Definisci il problema​

Definisci un problema MVC casuale generando un grafo con nodi pesati casualmente.

import networkx as nx
from sympy import symbols, Poly, srepr

# To change the weights, change the seed to any integer.
rng_seed = 18
_rng = np.random.default_rng(rng_seed)
node_count = 50
edge_probability = 0.08
mvc_graph = nx.erdos_renyi_graph(
node_count, edge_probability, seed=rng_seed, directed=False
)

# add node weights
for i in mvc_graph.nodes:
mvc_graph.add_node(i, weight=_rng.random())

# Optionally, visualize the graph
nx.draw_networkx(mvc_graph, nx.kamada_kawai_layout(mvc_graph), node_size=200)

Output della cella di codice precedente

Un modello di ottimizzazione standard per il MVC pesato può essere formulato come segue. Innanzitutto, è necessario aggiungere una penalità per ogni caso in cui un arco non sia connesso a un vertice nel sottoinsieme. Quindi, sia ni=1n_i = 1 se il vertice ii è nel cover (cioè nel sottoinsieme) e ni=0n_i = 0 altrimenti. In secondo luogo, l'obiettivo è minimizzare il numero totale di vertici nel sottoinsieme, che può essere rappresentato dalla seguente funzione:

Minimizey=∑i∈Vωini\textbf{Minimize}\qquad y = \sum_{i\in V} \omega_i n_i

# Construct the cost function.
variables = symbols([f"n[{i}]" for i in range(node_count)])
cost_function = Poly(0, variables)

for i in mvc_graph.nodes():
weight = mvc_graph.nodes[i].get("weight", 0)
cost_function += variables[i] * weight

Ora ogni arco nel grafo deve includere almeno un estremo del cover, il che può essere espresso con la disuguaglianza:

ni+nj≥1 for all (i,j)∈En_i + n_j \ge 1 \texttt{ for all } (i,j)\in E

Ogni caso in cui un arco non è connesso al vertice del cover deve essere penalizzato. Questo può essere rappresentato nella funzione di costo aggiungendo una penalità della forma P(1−ni−nj+ninj)P(1-n_i-n_j+n_i n_j) dove PP è una costante di penalità positiva. Quindi, un'alternativa non vincolata alla disuguaglianza vincolata per il MVC pesato è:

Minimizey=∑i∈Vωini+P(∑(i,j)∈E(1−ni−nj+ninj))\textbf{Minimize}\qquad y = \sum_{i\in V}\omega_i n_i + P(\sum_{(i,j)\in E}(1 - n_i - n_j + n_i n_j))

# Add penalty term.
penalty_constant = 2
for i, j in mvc_graph.edges():
cost_function += penalty_constant * (
1 - variables[i] - variables[j] + variables[i] * variables[j]
)

2. Esegui il problema​

# Solve the problem
mvc_job = solver.run(
problem=srepr(cost_function),
backend_name=backend_name, # E.g. "ibm_fez"
)

Controlla lo stato del workload della tua Qiskit Function o recupera i risultati come segue:

print(mvc_job.status())

3. Ottieni il risultato​

Recupera la soluzione e analizza i risultati. Poiché questo problema ha nodi pesati, la soluzione non è semplicemente il numero minimo di nodi coperti. Il costo della soluzione rappresenta invece la somma dei pesi dei vertici inclusi nel vertex cover, ovvero il "costo" o "peso" totale necessario per coprire tutti gli archi del grafo usando i vertici selezionati.

mvc_result = mvc_job.result()
qctrl_cost = mvc_result["solution_bitstring_cost"]

# Print results
print(f"Solution cost: {qctrl_cost}")
Solution cost: 10.248198273708624

Supporto​

Per qualsiasi domanda o problema, contatta Q-CTRL.

Passi successivi​