Skip to main content
IBM Quantum Platform

Optimization Solver : une fonction Qiskit par Q-CTRL Fire Opal

Consultez la documentation de l'API

Note

Les fonctions Qiskit sont une fonctionnalité expérimentale disponible uniquement pour IBM Quantum® Premium Plan, Flex Plan, et On-Prem (via IBM Quantum Platform API). Elles sont en cours de publication et peuvent être modifiées.

  • Le code présenté sur cette page a été développé en respectant les exigences suivantes. Nous vous recommandons d'utiliser ces versions ou des versions plus récentes.

    qiskit-ibm-runtime~=0.47.0
    sympy~=1.14.0
    

Aperçu

Avec le solveur d'optimisation Fire Opal, vous pouvez résoudre des problèmes d'optimisation à grande échelle sur du matériel quantique sans avoir besoin d'expertise quantique. Il suffit de saisir la définition du problème de haut niveau, et le Solveur s'occupe du reste. L'ensemble du flux de travail est sensible au bruit et tire parti de la gestion des performances de Fire Opal. Le Solveur fournit systématiquement des solutions précises à des problèmes classiques, même à l'échelle d'un appareil complet sur les plus grandes QPU IBM®.

Le solveur est polyvalent et permet de résoudre des problèmes d'optimisation combinatoire définis sous forme de fonctions objectives ou de graphes arbitraires. Il n'est pas nécessaire de faire correspondre les problèmes à la topologie des appareils. Il est possible de résoudre aussi bien les problèmes sans contraintes que ceux comportant des contraintes, ces dernières étant appliquées sous forme de contraintes « Hamming-weight-1 s strictes » plutôt que de termes de pénalité. Les exemples présentés dans ce guide montrent comment résoudre un problème d'optimisation à l'échelle industrielle, avec et sans contraintes, en utilisant différents types de données d'entrée pour le solveur. Le premier exemple porte sur un problème de coupe maximale défini sur un graphe régulier de degré 3 comportant 156 nœuds, tandis que le second exemple traite d'un problème de partitionnement de graphe à 50 nœuds défini par une fonction de coût.

Pour obtenir l'accès au solveur d'optimisation, contactez Q-CTRL.


Description de la fonction

Le Solver optimise et automatise entièrement l'algorithme, de la suppression des erreurs au niveau matériel à la cartographie efficace des problèmes et à l'optimisation classique en boucle fermée. En coulisses, le pipeline du Solveur réduit les erreurs à chaque étape, ce qui permet d'améliorer les performances requises pour une mise à l'échelle significative. Le processus sous-jacent s'inspire de l'algorithme d'optimisation approximative quantique (QAOA), qui est un algorithme hybride quantique-classique. Pour un résumé détaillé du flux de travail complet d'Optimization Solver, veuillez vous référer au manuscrit publié.

Visualisation du flux de travail du solveur d'optimisation

Pour résoudre un problème générique avec le solveur d'optimisation :

  1. Définissez votre problème sous la forme d'une fonction objective, d'un graphique ou d'une chaîne de spin ( SparsePauliOp ).
  2. Connectez-vous à la fonction via le catalogue de fonctions Qiskit.
  3. Exécutez le problème avec le Solveur et récupérez les résultats.

Formats de problèmes acceptés

  • Représentation de l'expression polynomiale d'une fonction objective. Idéalement créé dans Python avec un objet Poly existant SymPy et formaté dans une chaîne de caractères à l'aide de sympy.srepr.
  • Représentation graphique d'un type de problème spécifique. Le graphique doit être créé à l'aide de la bibliothèque networkx disponible à l'adresse Python. Il faut ensuite le convertir en chaîne de caractères à l'aide de la fonction networkx nx.readwrite.json_graph.adjacency_data.
  • Représentation en chaîne d'un problème spécifique. La chaîne de spin doit être représentée sous la forme d'un objet SparsePauliOp ; voir la documentation pour plus de détails.
Cette fonction prend-elle en charge tous les backends d' IBM?

Si vous souhaitez utiliser un backend qui n'est pas encore pris en charge par cette fonction, contactez Q-CTRL pour qu'il soit ajouté.


Tests de performances

Avis de non-responsabilité

Les performances peuvent dépendre à la fois de l'instance du problème et des étapes de traitement qui s'ensuivent. Dans certains cas, les échantillons classiques et les échantillons générés par des méthodes quantiques peuvent aboutir à une qualité de solution finale similaire après un post-traitement équivalent. L'évaluation doit donc prendre en compte l'ensemble du processus d'optimisation.

Les résultats d'étalonnage publiés montrent que le Solver résout avec succès des problèmes avec plus de 120 qubits, surpassant même les résultats précédemment publiés sur le recuit quantique et les dispositifs à ions piégés. Les mesures de référence suivantes donnent une indication approximative de la précision et de l'étendue des types de problèmes sur la base de quelques exemples. Les mesures réelles peuvent différer en fonction de diverses caractéristiques du problème, telles que le nombre de termes dans la fonction objective (densité) et leur localisation, le nombre de variables et l'ordre polynomial.

Le "nombre de qubits" indiqué n'est pas une limite absolue, mais représente des seuils approximatifs pour lesquels on peut s'attendre à une précision extrêmement constante des solutions. Des problèmes de plus grande taille ont été résolus avec succès, et les essais au-delà de ces limites sont encouragés.

La connectivité arbitraire des qubits est prise en charge pour tous les types de problèmes.

Type de problème
Nombre de qubits
Exemple
Exactitude
Durée totale (s)
Utilisation du temps d'exécution (s)
Nombre d'itérations
Problèmes quadratiques à connexions éparses1563-régulier max-cut100 %176429316
Optimisation binaire d'ordre supérieur156Modèle de verre de spin d'Ising100 %146127216
Problèmes quadratiques densément connectés50Max-Cut entièrement connecté100 %175826812
Problème sous contraintes avec des contraintes strictes50Partitionnement de graphes pondérés avec une densité d'arêtes de 8 %100 %107421510

Premiers pas

Commencez par vous authentifier à l'aide de votre clé API IBM Quantum. Ensuite, sélectionnez la fonction Qiskit comme suit. (Cet extrait de code part du principe que vous avez déjà enregistré votre compte dans votre environnement local.)

from qiskit_ibm_catalog import QiskitFunctionsCatalog

catalog = QiskitFunctionsCatalog(channel="ibm_quantum_platform")

# Verify that you have access to the function
catalog.list()

Output:

[QiskitFunction(qunova/hivqe-chemistry),
 QiskitFunction(global-data-quantum/quantum-portfolio-optimizer),
 QiskitFunction(algorithmiq/tem),
 QiskitFunction(qedma/qesem),
 QiskitFunction(multiverse/singularity),
 QiskitFunction(ibm/circuit-function),
 QiskitFunction(q-ctrl/optimization-solver),
 QiskitFunction(colibritd/quick-pde),
 QiskitFunction(q-ctrl/performance-management),
 QiskitFunction(kipu-quantum/iskay-quantum-optimizer)]
# Access Function
solver = catalog.load("q-ctrl/optimization-solver")

Exemple : optimisation sans contrainte

Résolvez le problème de la coupe maximale (max-cut). L'exemple suivant illustre les capacités du Solveur sur un problème de coupe maximale dans un graphe non pondéré à 3 arêtes régulières et 156 nœuds, mais vous pouvez également résoudre des problèmes sur des graphes pondérés.

En plus de qiskit-ibm-catalog, vous utiliserez également les paquets suivants pour exécuter cet exemple : networkx et numpy. Vous pouvez installer ces paquets en décommentant la cellule suivante si vous exécutez cet exemple dans un ordinateur portable utilisant le noyau IPython.

# %pip install networkx numpy

1. Définir le problème

Vous pouvez résoudre un problème de coupe maximale en définissant un problème de graphe et en spécifiant 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:

Output of the previous code cell

Le Solveur accepte une chaîne de caractères comme entrée de la définition du problème.

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

2. Exécutez le problème

Lors de l'utilisation de la méthode de saisie basée sur un graphique, spécifiez le type de problème.

# Solve the problem
maxcut_job = solver.run(
    problem=problem_as_str,
    problem_type="maxcut",
    backend_name=backend_name,  # E.g. "ibm_fez"
)

Pour vérifier l'état de votre charge de travail Qiskit Function ou obtenir les résultats, procédez comme suit :

# Print the ID so you can use it later, if necessary
print(maxcut_job.job_id)

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

Output:

34b53970-d95a-4e24-8763-fc6f3d112843
QUEUED

3. Récupérer le résultat

Récupérer la valeur de coupe optimale dans le dictionnaire des résultats.

Note

Le mappage des variables vers la chaîne de bits peut avoir changé. Le dictionnaire de sortie contient un variables_to_bitstring_index_map sous-dictionnaire qui aide à vérifier l'ordre.

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

Output:

Optimal cut value: 210.0

Vous pouvez vérifier l'exactitude du résultat en résolvant le problème de manière classique avec des solveurs open-source tels que PuLP si le graphe n'est pas densément connecté. Les problèmes de haute densité peuvent nécessiter des solveurs classiques avancés pour valider la solution.


Exemple : optimisation sous contraintes

L'exemple précédent de « max-cut » est un problème courant d'optimisation binaire quadratique sans contraintes. Le solveur d'optimisation de Q-CTRL peut également résoudre des problèmes d'optimisation sous contraintes en transmettant directement ces contraintes strictes au solveur via les données constraint d'entrée, au lieu de les encoder sous forme de termes de pénalité dans la fonction objectif. Le Solveur prend actuellement en charge les contraintes de type « Hamming-weight-1 » : chaque contrainte spécifie un groupe de variables dans lequel une seule variable doit être égale à 1, les autres devant être égales à 0.

L'exemple suivant montre comment définir une fonction de coût et un ensemble de contraintes strictes pour un problème d'optimisation sous contraintes, à savoir le partitionnement d'un graphe, en affectant chaque nœud d'un graphe à exactement un des groupes proposés, tout en minimisant le poids total des arêtes dont les extrémités appartiennent au même groupe.

En plus des paquets qiskit-ibm-catalog et qiskit , vous utiliserez également les paquets suivants pour exécuter cet exemple : numpy, networkx, et sympy. Vous pouvez installer ces paquets en décommentant la cellule suivante si vous exécutez cet exemple dans un ordinateur portable utilisant le noyau IPython.

# %pip install numpy networkx sympy

1. Définir le problème

Définir un problème de partitionnement aléatoire d'un graphe en générant un graphe dont les nœuds sont pondérés de manière aléatoire.

import networkx as nx
from sympy import Symbol, 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
graph = nx.erdos_renyi_graph(
    node_count, edge_probability, seed=rng_seed, directed=False
)

# add node weights
min_weight = -1.0
max_weight = 1.0
for i in graph.nodes:
    weight = (max_weight - min_weight) * _rng.random() + min_weight
    graph.add_node(i, weight=weight)

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

Output:

Output of the previous code cell

Un modèle d'optimisation standard pour le partitionnement de graphes pondérés peut être formulé comme suit. Divisons les nœuds du graphe en trois groupes : g∈{0,1,2}g \in \{0, 1, 2\}. Posons ni,g=1n_{i,g} = 1 si le nœud ii est affecté au groupe gg, et ni,g=0n_{i,g} = 0 dans le cas contraire. L'objectif est de minimiser le poids total des arêtes dont les extrémités sont attribuées au même groupe, le poids d'une arête (i,j)(i,j) étant le poids combiné de ses deux extrémités, ωi,j=ωi+ωj\omega_{i,j} = \omega_i + \omega_j :

Minimizey=∑(i,j)∈Eωi,j∑gni,g nj,g\textbf{Minimize}\qquad y = \sum_{(i,j)\in E} \omega_{i,j} \sum_{g} n_{i,g}\, n_{j,g}

# Construct the cost function.
group_count = 3
variables = [
    Symbol(f"n[{i},{g}]")
    for i in range(node_count)
    for g in range(group_count)
]
node_group_var = {
    (i, g): variables[i * group_count + g]
    for i in range(node_count)
    for g in range(group_count)
}
cost_function = Poly(0, *variables)

for i, j in graph.edges():
    edge_weight = graph.nodes[i]["weight"] + graph.nodes[j]["weight"]
    for g in range(group_count):
        cost_function += (
            edge_weight * node_group_var[(i, g)] * node_group_var[(j, g)]
        )

Chaque nœud doit être affecté à un seul et unique groupe parmi les trois. Il s'agit d'une contrainte de type « Hamming-weight-1 » : pour chaque nœud ii, exactement l'un des éléments ni,0,ni,1,ni,2n_{i,0}, n_{i,1}, n_{i,2} doit être égal à 1, et les autres doivent être égaux à 0 :

ni,0+ni,1+ni,2=1 for all i∈Vn_{i,0} + n_{i,1} + n_{i,2} = 1 \texttt{ for all } i \in V

Au lieu d'intégrer cette contrainte sous forme de terme de pénalité dans la fonction de coût, transmettez-la directement au Solveur en tant que contrainte stricte à l'aide du paramètre d'entrée constraint .

# Build the hard constraint: exactly one group per node.
constraint_dict = {
    str(tuple(f"n[{i},{g}]" for g in range(group_count))): 1
    for i in range(node_count)
}
print(f"Problem constraints: {constraint_dict}")

Output:

Problem constraints: {"('n[0,0]', 'n[0,1]', 'n[0,2]')": 1, "('n[1,0]', 'n[1,1]', 'n[1,2]')": 1, "('n[2,0]', 'n[2,1]', 'n[2,2]')": 1, "('n[3,0]', 'n[3,1]', 'n[3,2]')": 1, "('n[4,0]', 'n[4,1]', 'n[4,2]')": 1, "('n[5,0]', 'n[5,1]', 'n[5,2]')": 1, "('n[6,0]', 'n[6,1]', 'n[6,2]')": 1, "('n[7,0]', 'n[7,1]', 'n[7,2]')": 1, "('n[8,0]', 'n[8,1]', 'n[8,2]')": 1, "('n[9,0]', 'n[9,1]', 'n[9,2]')": 1, "('n[10,0]', 'n[10,1]', 'n[10,2]')": 1, "('n[11,0]', 'n[11,1]', 'n[11,2]')": 1, "('n[12,0]', 'n[12,1]', 'n[12,2]')": 1, "('n[13,0]', 'n[13,1]', 'n[13,2]')": 1, "('n[14,0]', 'n[14,1]', 'n[14,2]')": 1, "('n[15,0]', 'n[15,1]', 'n[15,2]')": 1, "('n[16,0]', 'n[16,1]', 'n[16,2]')": 1, "('n[17,0]', 'n[17,1]', 'n[17,2]')": 1, "('n[18,0]', 'n[18,1]', 'n[18,2]')": 1, "('n[19,0]', 'n[19,1]', 'n[19,2]')": 1, "('n[20,0]', 'n[20,1]', 'n[20,2]')": 1, "('n[21,0]', 'n[21,1]', 'n[21,2]')": 1, "('n[22,0]', 'n[22,1]', 'n[22,2]')": 1, "('n[23,0]', 'n[23,1]', 'n[23,2]')": 1, "('n[24,0]', 'n[24,1]', 'n[24,2]')": 1, "('n[25,0]', 'n[25,1]', 'n[25,2]')": 1, "('n[26,0]', 'n[26,1]', 'n[26,2]')": 1, "('n[27,0]', 'n[27,1]', 'n[27,2]')": 1, "('n[28,0]', 'n[28,1]', 'n[28,2]')": 1, "('n[29,0]', 'n[29,1]', 'n[29,2]')": 1, "('n[30,0]', 'n[30,1]', 'n[30,2]')": 1, "('n[31,0]', 'n[31,1]', 'n[31,2]')": 1, "('n[32,0]', 'n[32,1]', 'n[32,2]')": 1, "('n[33,0]', 'n[33,1]', 'n[33,2]')": 1, "('n[34,0]', 'n[34,1]', 'n[34,2]')": 1, "('n[35,0]', 'n[35,1]', 'n[35,2]')": 1, "('n[36,0]', 'n[36,1]', 'n[36,2]')": 1, "('n[37,0]', 'n[37,1]', 'n[37,2]')": 1, "('n[38,0]', 'n[38,1]', 'n[38,2]')": 1, "('n[39,0]', 'n[39,1]', 'n[39,2]')": 1, "('n[40,0]', 'n[40,1]', 'n[40,2]')": 1, "('n[41,0]', 'n[41,1]', 'n[41,2]')": 1, "('n[42,0]', 'n[42,1]', 'n[42,2]')": 1, "('n[43,0]', 'n[43,1]', 'n[43,2]')": 1, "('n[44,0]', 'n[44,1]', 'n[44,2]')": 1, "('n[45,0]', 'n[45,1]', 'n[45,2]')": 1, "('n[46,0]', 'n[46,1]', 'n[46,2]')": 1, "('n[47,0]', 'n[47,1]', 'n[47,2]')": 1, "('n[48,0]', 'n[48,1]', 'n[48,2]')": 1, "('n[49,0]', 'n[49,1]', 'n[49,2]')": 1}
Problèmes partiellement contraints

Il n'est pas nécessaire d'ajouter toutes les variables à constraint. Toute variable qui n'est pas incluse dans le dictionnaire reste non contrainte; vous pouvez donc combiner, dans un même problème, des groupes de variables soumis à des contraintes strictes et des variables libres.

2. Exécutez le problème

# Solve the problem
partition_job = solver.run(
    problem=srepr(cost_function),
    constraint=constraint_dict,
    backend_name="ibm_marrakesh",  # E.g. "ibm_marrakesh"
)

Pour vérifier l'état de votre charge de travail Qiskit Function ou obtenir les résultats, procédez comme suit :

# Print the ID so you can use it later, if necessary
print(partition_job.job_id)

# Get job status
print(partition_job.status())

Output:

b8085944-f313-444e-be39-ea61b1b47ebd
QUEUED

3. Obtenir le résultat

Récupérez la solution et analysez les résultats. Le coût de la solution correspond au poids total des arêtes dont les extrémités se sont retrouvées dans le même groupe; ainsi, un coût plus faible indique un meilleur partitionnement du graphe.

partition_result = partition_job.result()
qctrl_cost = partition_result["solution_bitstring_cost"]
solution_bitstring = partition_result["solution_bitstring"]

# Print results
print(f"Total weight of same-group edges: {qctrl_cost}")
print(f"Solution bitstring: {solution_bitstring}")

Output:

Total weight of same-group edges: -36.5539
Solution bitstring: 100100100100100001100100100100100100100100100100100001010100010100100100100010001001100100100001100001100001010001001010100100100100100010100100100100

Obtenir de l'aide

Pour toute question ou problème, contactez Q-CTRL.


Journal des modifications

  • 10 août 2026 : Ajout de la prise en charge des contraintes « dures » (poids de Hamming égal à 1) via le paramètre constraint d'entrée, et mise à jour de l'exemple d'optimisation sous contraintes pour qu'il les utilise.
  • 11 février 2026 : Nous prenons désormais en charge ibm_miami

Etapes suivantes

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