Étendre Qiskit à Python avec C
L'API C de Qiskit peut être utilisée dans les modules d'extension d' Python. Vous pouvez écrire les parties de vos extensions Qiskit qui sont critiques pour les performances en C afin de les accélérer, puis les distribuer en toute sécurité à vos utilisateurs.
Ce guide vous explique comment définir un module d'extension complet, configurer
son processus de compilation et le mettre à la disposition des utilisateurs d' Python. Ce paquet propose un portage simple
desAddSpectatorMeasures modules complémentaires de Qiskit vers le langage C. Il s'agit d'un véritable passeur personnalisé
qui trouve une application concrète dans les extensions de Qiskit.
Les ressources externes suivantes pourraient vous être utiles :
- La documentation CPython sur la création de modules d'extension.
- La documentation d' NumPy s concernant l'utilisation de son API C.
L'API C de Qiskit est mise à disposition des modules d'extension d' Python de manière très similaire à l'API C d' NumPy. Si vous avez déjà programmé une extension « NumPy », le processus Qiskit vous semblera familier.
L'API C de Qiskit est encore au stade expérimental. Il n'existe donc pas encore d'interface de programmation ou binaire pleinement stable, et des changements susceptibles de briser la compatibilité peuvent survenir entre les versions mineures.
Par exemple, un module d'extension utilisant Qiskit v2.4.0 lors de la compilation est assuré de fonctionner avec Qiskit v2.4.1 lors de l'exécution, mais risque de ne plus fonctionner si l'on utilise Qiskit v2.5.0 lors de l'exécution.
Exigences
Suivez les instructions de la rubrique « Installation » pour installer les éléments suivants :
- La chaîne d'outils de compilation C standard pour votre plateforme
- Une version de
Pythonqui inclut ses en-têtes d'API C.
Vous devez également connaître, ou être prêt à rechercher, les fonctions et les objets disponibles dans l 'API C de Qiskit, et avoir une certaine connaissance de la programmation en C.
Commencez avec un répertoire vide.
Créer la structure de répertoires
Nous utiliserons une structure de srcrépertoires basée sur et un système de compilation simple setuptoolsbasé sur. Ces
instructions devraient pouvoir s'adapter facilement à n'importe quel système de compilation capable de compiler
des modules d'extension.
La structure finale ressemblera à ceci :
extension-module
├── pyproject.toml
├── setup.py
└── src
└── spectator_measures
├── __init__.py
└── _coremodule.cEn résumé :
pyproject.tomldéfinit les métadonnées statiques standard relatives au paquet Python que nous sommes en train de créer, notamment son nom, son auteur et ses dépendances de compilation et d'exécution.setup.pycontient la configuration dynamique minimale dont nous avons besoin pour créer notre module d'extension.src/spectator_measures/__init__.pydéfinit l'interface utilisateur et fournit du code permettant de s'interfacer avec les composants de l'espace de données « Python » de Qiskit.src/spectator_measures/_coremodule.cdéfinit le module d'extension C, qui contiendra tout le code critique pour les performances de notre paquet.
Nous examinerons chaque fichier en détail, en constituant le paquet avec son module d'extension.
Définir les métadonnées du paquet
Commencez par définir le pyproject.toml fichier. C'est la norme pour un setuptoolsprojet basé sur
, bien qiskit que soit une exigence supplémentaire dans le build-system.requires tableau,
en plus de setuptools.
[build-system]
requires = [
"setuptools",
"qiskit~=2.4.0",
]
build-backend = "setuptools.build_meta"
[project]
name = "spectator_measures"
authors = [
{ name = "Qiskit Developer" },
]
version = "0.0.1"
dependencies = [
"qiskit~=2.4.0",
]
# If you intend to release your package, you should
# also set the `license` information, and so on.
[tool.setuptools]
package-dir = {"" = "src"}Définissez la version d'exécution de Qiskit de
project.dependencies manière à ce qu'elle corresponde à la version mineure utilisée lors de la compilation.
Dans de nombreux projets basés uniquement sur setuptoolsPython, il suffirait d'avoir le
pyproject.toml fichier. Cependant, notre module doit pouvoir accéder aux fichiers d'en-tête de l'API C de Qiskit pendant
son processus de compilation. À partir de la version v2.4, ces éléments sont inclus dans les distributions Qiskit SDK Python.
Pour localiser le répertoire qui les contient, exécutez qiskit.capi.get_include().
On obtient ainsi un setup.py fichier qui ressemble à ceci :
import qiskit
from setuptools import setup, Extension
core_ext = Extension(
# The fully qualified module name of the extension.
name="spectator_measures._core",
# The C source files needed for the extension. The file
# name is conventionally `<mod>module.c`, where `<mod>`
# is the module name (`_core`, in this case).
sources=["src/spectator_measures/_coremodule.c"],
# Directories containing additional header files used in
# the build process.
include_dirs=[qiskit.capi.get_include()],
)
setup(ext_modules=[core_ext])La plupart des informations relatives au paquet sont définies dans pyproject.toml, et setuptools.setup() va
également lire ce fichier.
Consultez le Guide setuptools de l'utilisateur pour plus
d'informations sur la configuration des setuptoolsprojets basés sur.
Écrire le wrapper « -space » pour la commande « Python »
D'un point de vue technique, il est possible de définir tous les éléments d'une extension Python à partir du langage C. Dans la pratique, il est plus facile d'interagir avec d'autres codes de l'espace « Python » depuis Python lui-même.
Ce paquet définit un passage de transcompilation personnalisé qui dérive de la classe « Python
qiskit.transpiler.TransformationPass -space », mais qui utilise une fonction du module d'extension C pour
l'ensemble de sa logique métier. Cela ressemble à ceci :
from qiskit.transpiler import TransformationPass, Target
from . import _core
__version__ = "0.0.1"
__all__ = ["AddSpectatorMeasures"]
class AddSpectatorMeasures(TransformationPass):
def __init__(
self,
target: Target,
*,
include_unmeasured: bool = False,
creg_name: str | None = None,
add_barrier: bool = True
):
super().__init__()
self.target = target
self.include_unmeasured = include_unmeasured
self.creg_name = creg_name
self.add_barrier = add_barrier
def run(self, dag):
# Delegate to our C extension module.
_core.add_spectator_measures(
dag,
self.target,
include_unmeasured=self.include_unmeasured,
creg_name=self.creg_name,
add_barrier=self.add_barrier,
)
return dagLes détails précis de ce pass n'ont pas d'importance dans le cadre de ce guide. Si cela vous intéresse, vous pouvez
consulter le AddSpectatorMeasures Documentation de l'API en
qiskit-addon-utils. Ce guide propose un portage simple de ce passeur,
sans prise en charge des opérations de contrôle de flux.
Écrire le module d'extension C
Les ressources suivantes pourraient vous être utiles :
Cette section traite de l'extension C proprement dite. Il s'agit du fichier le plus complexe du projet; nous allons donc le diviser en plusieurs étapes.
Configurer les fichiers d'en-tête
Lorsque vous développez un module d'extension pour Python, vous devez inclure ce fichier Python.h avant tout autre fichier.
Pour utiliser l'API C de Qiskit dans un module d'extension, vous devez définir la macro
QISKIT_PYTHON_EXTENSION avant de l'inclure qiskit.h.
Nos inclus se présentent alors comme suit :
#define QISKIT_PYTHON_EXTENSION
#include <Python.h>
#include <qiskit.h>
#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>Écrivez le code de l'API en C pur
Ensuite, écrivez toute la logique métier sous forme de code API C pur de Qiskit. Nous présenterons cette logique à l'espace d' Python s dans la section suivante.
Cette section ne contient que du code API C pur de Qiskit. Il utilise les types de l'API C :
QkDag *, correspondant à l'espace PythonDAGCircuit.QkTarget *, correspondant à l'espace PythonTarget.QkNeighbors, un type de l'API C natif représentant les contraintes de couplage entre deux qubits.QkCircuitInstruction, un type d'API C natif permettant d'interroger des instructions individuelles.
Les deux premières font partie de notre interaction avec l'espace « Python », mais lorsque nous les utilisons, nous n'avons besoin de prendre en compte que l'API C pure. Ce code ne fait appel à l'interpréteur d' Python .
Notez que toutes les fonctions et tous les symboles définis dans cette section sont déclarés avec static le lien.
En effet, l'interpréteur Python ne se liera pas à ce module d'extension; nous fournirons
à l'interpréteur les détails des fonctions disponibles dans la section suivante.
Nous ne nous attarderons pas sur les détails algorithmiques de ce code; il est utile d'utiliser un passage de transpileur pertinent pour la démonstration, mais la mise en œuvre précise de l'algorithme n'est pas importante pour ce guide.
/**
* The default name to use for `creg_name` if none is supplied.
*/
static char DEFAULT_CREG_NAME[] = "spec";
/**
* Is there a 2q link from the given qubit to any active qubit?
*/
static bool adjacent_to_active(QkNeighbors *adj, uint32_t qubit,
bool *active) {
for (uint32_t offset = adj->partition[qubit];
offset < adj->partition[qubit + 1]; offset++) {
if (active[adj->neighbors[offset]]) {
return true;
}
}
return false;
}
/**
* A transpiler pass that adds terminal measurements to all "spectator"
* qubits.
*/
static uint32_t add_spectator_measures(QkDag *dag,
const QkTarget *target,
bool include_unmeasured,
const char *creg_name,
bool add_barrier) {
uint32_t num_spectators = 0;
uint32_t num_qubits = qk_dag_num_qubits(dag);
uint32_t num_instructions = qk_dag_num_op_nodes(dag);
bool *active = calloc(num_qubits, sizeof(*active));
bool *is_additional_spectator =
calloc(num_qubits, sizeof(*is_additional_spectator));
uint32_t *spectators = malloc(num_qubits * sizeof(*spectators));
uint32_t *topological =
malloc(num_instructions * sizeof(*topological));
QkNeighbors neighbors;
QkCircuitInstruction instruction;
qk_neighbors_from_target(target, &neighbors);
qk_dag_topological_op_nodes(dag, topological);
for (uint32_t i = 0; i < num_instructions; i++) {
qk_dag_get_instruction(dag, topological[i], &instruction);
if (!strcmp(instruction.name, "barrier")) {
// Barriers don't count for the purposes of determining
// final measurements, either.
qk_circuit_instruction_clear(&instruction);
continue;
}
// If we're not adding measurements to "unmeasured" active
// qubits, then nothing counts as an additional "maybe
// spectator". If we are, then it's a maybe spectator if its
// last visited instruction was not a measure.
bool additional_spectator =
include_unmeasured && strcmp(instruction.name, "measure");
for (uint32_t *qarg = instruction.qubits;
qarg != instruction.qubits + instruction.num_qubits;
qarg++) {
active[*qarg] = true;
is_additional_spectator[*qarg] = additional_spectator;
}
qk_circuit_instruction_clear(&instruction);
}
for (uint32_t qubit = 0; qubit < num_qubits; qubit++) {
bool is_spectator =
!active[qubit] &&
adjacent_to_active(&neighbors, qubit, active);
is_spectator = is_spectator || is_additional_spectator[qubit];
if (is_spectator) {
spectators[num_spectators] = qubit;
num_spectators += 1;
}
}
if (num_spectators) {
uint32_t clbit = qk_dag_num_clbits(dag);
creg_name = creg_name ? creg_name : DEFAULT_CREG_NAME;
QkClassicalRegister *creg =
qk_classical_register_new(num_spectators, creg_name);
qk_dag_add_classical_register(dag, creg);
qk_classical_register_free(creg);
if (add_barrier) {
qk_dag_apply_barrier(dag, NULL, num_qubits, false);
}
for (uint32_t i = 0; i < num_spectators; i++) {
qk_dag_apply_measure(dag, spectators[i], clbit + i, false);
}
}
qk_neighbors_clear(&neighbors);
free(topological);
free(spectators);
free(is_additional_spectator);
free(active);
return num_spectators;
}Écrire le code d'interaction avec l' Python
Toute la logique métier est désormais définie en C pur. Ensuite, il faut l'exposer en toute sécurité à l' Python
Pour commencer, définissez la seule fonction qui sera accessible à l' Python. Cela doit
respecter une signature définie, qui se compose uniquement de types d' Python s ressemblant à une
fn(self, *args, **kwargs) méthode. Nous devons renvoyer un PyObject *, qui est la forme générique de
tout objet de type Python.
La fonction complète se présente comme suit :
static PyObject *py_add_spectator_measures(PyObject *self,
PyObject *args,
PyObject *kwargs) {
// Define space to hold the C-native handles we will parse out of the
// Python-space inputs.
QkDag *dag;
QkTarget *target;
const char *creg_name;
int include_unmeasured, add_barrier;
// This `kwlist` and `PyArg_Parse*` setup is standard Python C API
// programming for extension modules. We will examine the use of
// Qiskit C API functions within it afterwards.
static char *const kwlist[] = {
"dag", "target", "include_unmeasured",
"creg_name", "add_barrier", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&O&|pzp", kwlist,
qk_dag_convert_from_python, &dag,
qk_target_convert_from_python,
&target, &include_unmeasured,
&creg_name, &add_barrier)) {
// An error has occurred. The Python exception state will already
// be set, so we need to return the error indicator.
return NULL;
}
// Now we have C-native types, we can delegate to our C logic.
add_spectator_measures(dag, target, include_unmeasured, creg_name,
add_barrier);
Py_RETURN_NONE;
}En bref, la fonction :
- Respecte une signature définie pour accepter des arguments d' Python s arbitraires.
- Définit l'espace nécessaire au stockage des objets natifs C extraits des arguments de l' Python.
- Appelle une fonction d'analyse syntaxique pour extraire les objets natifs C, configurée avec la liste des arguments attendus, des arguments par mot-clé et des fonctions à utiliser pour les convertir. Si cela échoue, la fonction propage l'erreur.
- Délègue à la logique métier native C décrite dans la section précédente, qui modifie le DAG directement.
- Renvoie l'objet « Python -space
None».
C'est là que se trouve toute la logique PyArg_ParseTupleAndKeywords la plus complexe. Ceci est clairement expliqué dans la
documentation de CPython consacrée à l'analyse des arguments, que vous
devriez consulter pour plus d'informations.
L'API C de Qiskit fournit plusieurs fonctions portant des noms tels que qk_*_convert_from_python, qui sont
conçues comme des fonctions de « conversion » destinées à être utilisées avec PyArg_Parse*des fonctions. Celles-ci correspondent aux
O& clés de la chaîne de format; ici, nous avons utilisé qk_dag_convert_from_python et
qk_target_convert_from_python. Ces fonctions empruntent
l'objet natif C à l'argument «Python
» dont elles sont dérivées. Cela signifie que les mutations se répercuteront sur l'espace d' Python, mais aussi que
vous devez veiller à ne pas libérer votre référence à l'objet d' Python qui les sous-tend, pendant que vous utilisez
le résultat. C'est la norme pour la programmation via l'API C d' Python.
Nous allons ensuite définir les informations relatives à ce module et à la fonction qu'il contient, afin de pouvoir les transmettre à l'espace d' Python :
static PyMethodDef core_methods[] = {
// This entry is our function, cast to the correct type.
{"add_spectator_measures",
(PyCFunction)(void (*)(void))py_add_spectator_measures,
METH_VARARGS | METH_KEYWORDS, ""},
// A sentinel marking the end of the list.
{NULL, NULL, 0, NULL},
};
static struct PyModuleDef core_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "_core",
.m_methods = core_methods,
};Cette table de méthodes et cette structure de définition de module sont décrites plus en détail dans la documentation CPython consacrée à l'initialisation des modules.
Enfin, indiquez à Python comment initialiser le module. C'est la seule fonction du fichier C
qui est exportée. Son nom doit correspondre exactement au modèle
PyInit_<mod>, où <mod> est le nom (non qualifié) du module. Dans ce cas, le nom complet
du module est spectator_measures._core, et le nom non qualifié est _core, notre
fonction doit donc s'appeler PyInit__core, avec le double trait de soulignement.
PyMODINIT_FUNC PyInit__core(void) {
// This line is critical to use the Qiskit C API. Your code will
// likely be immediately terminated by the operating system if you
// forget to do this.
if (qk_import() < 0) {
return NULL;
};
// The standard Python call to initialize a module.
return PyModuleDef_Init(&core_module);
}Les symboles PyMODINIT_FUNC``PyModuleDef_Init et sont tous deux courants dans la programmation de l'API C d' Python. Le
composant spécifique à Qiskit est qk_import(). Il est essentiel que vous appeliez cette fonction au cours de la
fonction d'initialisation de votre module; vous ne pourrez appeler aucune fonction de l'API C de Qiskit
tant que celle-ci n'aura pas été exécutée avec succès.
Utilisez le paquet disponible à l'adresse Python
Il s'agit désormais d'un ensemble complet, comprenant un module d'extension C. Comme seuls des outils standard ont été utilisés et qu'aucune bibliothèque système non standard n'est intégrée lors de la compilation, le processus de compilation est simple.
Vous pouvez utiliser n'importe quel outil de compilation de type « PEP-517-compatible ». À titre d'exemple simple, vous pouvez exécuter la commande suivante à la racine du référentiel pour installer le paquet.
pip install .Cela permet de compiler le module d'extension C et d'installer le paquet complet « Python » dans votre environnement.
Voici un exemple d'utilisation de ce passage de transcompilation personnalisé :
from qiskit import QuantumCircuit
from qiskit.transpiler import CouplingMap, Target
from spectator_measures import AddSpectatorMeasures
num_qubits = 10
qc = QuantumCircuit(num_qubits)
qc.x(0)
qc.x(5)
target = Target.from_configuration(
basis_gates=["x", "sx", "rz", "cx"],
num_qubits=num_qubits,
coupling_map=CouplingMap.from_line(num_qubits),
)
pass_ = AddSpectatorMeasures(target)
pass_(qc).draw()Il en résulte :
┌───┐ ░
q_0: ┤ X ├─░──────────
└───┘ ░ ┌─┐
q_1: ──────░─┤M├──────
░ └╥┘
q_2: ──────░──╫───────
░ ║
q_3: ──────░──╫───────
░ ║ ┌─┐
q_4: ──────░──╫─┤M├───
┌───┐ ░ ║ └╥┘
q_5: ┤ X ├─░──╫──╫────
└───┘ ░ ║ ║ ┌─┐
q_6: ──────░──╫──╫─┤M├
░ ║ ║ └╥┘
q_7: ──────░──╫──╫──╫─
░ ║ ║ ║
q_8: ──────░──╫──╫──╫─
░ ║ ║ ║
q_9: ──────░──╫──╫──╫─
░ ║ ║ ║
spec: 3/═════════╩══╩══╩═
0 1 2