Optimization Solver : une fonction Qiskit par Q-CTRL Fire Opal
Consultez la documentation de l'API
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é.
Pour résoudre un problème générique avec le solveur d'optimisation :
- Définissez votre problème sous la forme d'une fonction objective, d'un graphique ou d'une chaîne de spin (
SparsePauliOp). - Connectez-vous à la fonction via le catalogue de fonctions Qiskit.
- 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.
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
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 éparses | 156 | 3-régulier max-cut | 100 % | 1764 | 293 | 16 |
| Optimisation binaire d'ordre supérieur | 156 | Modèle de verre de spin d'Ising | 100 % | 1461 | 272 | 16 |
| Problèmes quadratiques densément connectés | 50 | Max-Cut entièrement connecté | 100 % | 1758 | 268 | 12 |
| Problème sous contraintes avec des contraintes strictes | 50 | Partitionnement de graphes pondérés avec une densité d'arêtes de 8 % | 100 % | 1074 | 215 | 10 |
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 numpy1. 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:
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.
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 sympy1. 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:
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 : . Posons si le nœud est affecté au groupe , et 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 étant le poids combiné de ses deux extrémités, :
# 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 , exactement l'un des éléments doit être égal à 1, et les autres doivent être égaux à 0 :
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}
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
constraintd'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
- Demander l'accès au solveur d'optimisation Q-CTRL.
- Consultez la documentation de l'API relative à cette fonction Qiskit.
- Essayez le tutoriel Résoudre des problèmes d'optimisation binaire d'ordre supérieur avec le solveur d'optimisation de Q-CTRL.
- Critique Sachdeva, N., et al. (2024). L'optimisation quantique à l'aide d'un ordinateur quantique à 127 qubits utilisant un modèle de porte IBM peut surpasser les annealeurs quantiques pour les problèmes d'optimisation binaire non triviaux. arXiv prépublication arXiv:2406.01743.
- Critique Loco, D., et al. (2026). Prédiction pratique des sites d'hydratation des poches protéiques pour la découverte de médicaments sur un ordinateur quantique. arXiv prépublication arXiv:2512.08390.
- Consultez l'étude de cas Mazda.
- Consultez l'étude de cas Network Rail.
- Examinez l'étude de cas sur l'armée australienne.
- Consultez l'étude de cas Transport for New South Wales.