Étendre Qiskit en Python avec C
L'API C de Qiskit peut être utilisée au sein de modules d'extension Python. Tu peux écrire les sections critiques en termes de performances de tes extensions Qiskit en C pour les accélérer, et les distribuer en toute sécurité à tes utilisateurs.
Ce guide te présente le processus de définition d'un module d'extension complet, de configuration
de son processus de compilation, et de son exposition aux utilisateurs Python. Le package fournit un simple portage de
AddSpectatorMeasures des extensions Qiskit en C. Il s'agit d'une vraie passe personnalisée
avec un vrai cas d'utilisation dans les extensions Qiskit.
Tu pourrais trouver les ressources externes suivantes utiles :
- La documentation CPython sur l'écriture de modules d'extension.
- La documentation NumPy sur l'utilisation de son API C.
L'API C de Qiskit est exposée pour les modules d'extension Python d'une manière très similaire à l'API C de NumPy. Si tu as déjà programmé une extension NumPy, tu trouveras le processus Qiskit familier.
L'API C de Qiskit est encore expérimentale. Ainsi, il n'existe pas encore d'interface de programmation ou binaire pleinement stable, et des changements incompatibles peuvent survenir entre les versions mineures.
Par exemple, un module d'extension utilisant Qiskit v2.4.0 lors de la compilation est garanti de fonctionner avec Qiskit v2.4.1 à l'exécution, mais peut ne plus fonctionner avec Qiskit v2.5.0 à l'exécution.
Prérequis
Commence à partir d'un répertoire vide.
Tu dois disposer de la chaîne d'outils de compilation C standard pour ta plateforme. Tu dois également avoir une version de Python qui inclut ses en-têtes d'API C (c'est standard).
Tu devrais être familier avec, ou prêt à consulter, les fonctions et objets individuels disponibles dans l'API C Qiskit. Tu devrais avoir une certaine familiarité avec la programmation C.
Créer la structure de répertoires
Nous utiliserons une structure de répertoires basée sur src et un système de compilation simple basé sur setuptools. Ces
instructions devraient être faciles à adapter à n'importe quel système de compilation capable de construire
des modules d'extension.
La structure finale ressemblera à :
extension-module
├── pyproject.toml
├── setup.py
└── src
└── spectator_measures
├── __init__.py
└── _coremodule.c
En résumé :
pyproject.tomldéfinit les métadonnées statiques standard sur le package Python que nous créons, notamment son nom, son auteur et ses dépendances au moment de la compilation et de l'exécution.setup.pycontient la configuration dynamique minimale nécessaire pour compiler notre module d'extension.src/spectator_measures/__init__.pydéfinit l'interface utilisateur et fournit du code pour interagir avec les composants Python-space de Qiskit.src/spectator_measures/_coremodule.cdéfinit le module d'extension C, qui contiendra tout le code critique en termes de performances de notre package.
Nous examinerons chaque fichier en détail, en construisant le package avec son module d'extension.
Définir les métadonnées du package
Commence par définir le fichier pyproject.toml. C'est standard pour un projet basé sur setuptools,
bien que qiskit soit une dépendance supplémentaire dans le tableau build-system.requires,
en plus de setuptools.
pyproject.toml
[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"}
À partir de Qiskit v2.4, l'API C n'est pas encore stable en dehors des versions mineures (par exemple, l'API C pour v2.4.0 sera
compatible avec v2.4.1 mais pas avec v2.5.0). À l'avenir, nous prévoyons d'étendre
cette stabilité aux versions majeures. Pour l'instant, définis la version d'exécution de Qiskit dans
project.dependencies pour qu'elle corresponde à la version mineure utilisée lors de la compilation.
Dans de nombreux projets purement Python basés sur setuptools, le fichier pyproject.toml serait suffisant. Cependant, notre module a besoin d'accéder aux fichiers d'en-tête de l'API C de Qiskit lors
de son processus de compilation. À partir de v2.4, ceux-ci sont inclus dans les distributions Python du SDK Qiskit.
Pour localiser le répertoire qui les contient, exécute qiskit.capi.get_include().
Cela produit un fichier setup.py qui ressemble à :
setup.py
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 du package sont définies dans pyproject.toml, et setuptools.setup() lira également ce fichier.
Consulte le Guide utilisateur de setuptools pour plus
d'informations sur la configuration des projets basés sur setuptools.
Écrire le wrapper Python-space
Il est techniquement possible de tout définir dans une extension Python depuis C. En pratique, il est plus facile d'interagir avec d'autres codes Python-space depuis Python lui-même.
Ce package définit une passe de transpileur personnalisée qui dérive de la classe Python-space
qiskit.transpiler.TransformationPass, mais utilise une fonction du module d'extension C pour
toute sa logique métier. Cela ressemble à :
src/spectator_measures/__init__.py
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 dag
Les détails exacts de cette passe ne sont pas importants pour ce guide. Si tu es intéressé, tu peux
consulter la documentation API de AddSpectatorMeasures dans
qiskit-addon-utils. Ce guide produit un simple portage de cette passe,
sans prise en charge des opérations de flux de contrôle.
Écrire le module d'extension C
Tu pourrais trouver les ressources suivantes utiles :
Cette section concerne l'extension C proprement dite. C'est le fichier le plus complexe du projet, nous le diviserons donc en étapes.
Configurer les fichiers d'en-tête
Lors de la compilation d'un module d'extension Python, tu dois inclure Python.h avant tout autre fichier.
Pour utiliser l'API C de Qiskit dans un module d'extension, tu dois définir la macro
QISKIT_PYTHON_EXTENSION avant d'inclure qiskit.h.
Nos inclusions ressemblent alors à :
src/spectator_measures/_coremodule.c
#define QISKIT_PYTHON_EXTENSION
#include <Python.h>
#include <qiskit.h>
#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
Écrire le code pur de l'API C
Ensuite, écris toute la logique métier comme du code pur de l'API C Qiskit. Nous exposerons cette logique à Python-space dans la section suivante.
Cette section contient uniquement du code pur de l'API C Qiskit. Elle utilise les types de l'API C :
QkDag *, correspondant àDAGCircuitdans Python-space.QkTarget *, correspondant àTargetdans Python-space.QkNeighbors, un type natif de l'API C représentant les contraintes de couplage à deux qubits.QkCircuitInstruction, un type natif de l'API C pour interroger des instructions individuelles.
Les deux premiers font partie de notre interaction avec Python-space, mais lorsqu'on travaille avec eux, nous n'avons besoin de considérer que l'API C pure. Il n'y a pas d'interaction avec l'interpréteur Python dans ce code.
Note que toutes les fonctions et symboles définis dans cette section sont déclarés avec une liaison static.
C'est parce que l'interpréteur Python ne se liera pas contre 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 instructif d'utiliser une vraie passe de transpileur pour la démonstration, mais l'implémentation précise de l'algorithme n'est pas importante pour ce guide.
src/spectator_measures/_coremodule.c (appended)
/**
* 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 Python
Toute la logique métier est maintenant définie en C pur. Elle doit ensuite être exposée en toute sécurité à Python.
Pour commencer, définit la seule fonction qui sera exposée à Python. Celle-ci doit
suivre une signature définie, qui est purement en termes de types Python ressemblant à une
méthode fn(self, *args, **kwargs). Nous devons retourner un PyObject *, qui est la forme générique de
tout objet Python.
La fonction complète ressemble à :
src/spectator_measures/_coremodule.c (appended)
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 résumé, la fonction :
- Suit une signature définie pour accepter des arguments Python arbitraires.
- Définit de l'espace pour stocker les objets natifs C extraits des arguments Python.
- Appelle une fonction d'analyse pour extraire les objets natifs C, configurée avec la liste des arguments attendus, des arguments par mots-clés, et des fonctions à utiliser pour les convertir. En cas d'échec, la fonction propage l'erreur.
- Délègue à la logique métier native C de la section précédente, qui mute le DAG sur place.
- Retourne l'objet Python-space
None.
La logique la plus complexe est entièrement dans PyArg_ParseTupleAndKeywords. Celle-ci est bien documentée dans la
documentation CPython sur l'analyse des arguments, que tu
devrais consulter pour plus d'informations.
L'API C de Qiskit fournit plusieurs fonctions portant des noms comme qk_*_convert_from_python, qui sont
conçues comme des fonctions "convertisseurs" à utiliser avec les fonctions PyArg_Parse*. Elles correspondent aux
clés O& dans 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 à partir de l'argument Python dont elles sont dérivées. Cela signifie que les mutations se propageront à Python-space, mais aussi que
tu dois prendre soin de ne pas libérer ta référence à l'objet Python qui les supporte pendant leur utilisation. C'est standard pour la programmation de l'API C Python.
Ensuite, nous définissons les informations sur ce module et la fonction qu'il contient, pour pouvoir les transmettre à Python-space :
src/spectator_measures/_coremodule.c (appended)
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 sur la table de méthodes du module et la fonction d'initialisation.
Enfin, indique à Python comment initialiser le module. C'est la seule fonction dans le fichier C
qui est exportée. Son nom doit correspondre exactement au motif
PyInit_<mod>, où <mod> est le nom du module (non qualifié). Dans ce cas, le nom de module entièrement
qualifié est spectator_measures._core, et le nom non qualifié est _core, donc notre
fonction doit s'appeler PyInit__core, avec le double tiret bas.
src/spectator_measures/_coremodule.c (appended)
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 et PyModuleDef_Init sont tous deux des éléments standard de la programmation de l'API C Python. Le
composant spécifique à Qiskit est qk_import(). Il est essentiel d'appeler cette fonction lors de la
fonction d'initialisation de ton module ; tu ne pourras appeler aucune fonction de l'API C Qiskit
avant que cette exécution ait réussi.
Utiliser le package depuis Python
C'est maintenant un package complet, incluant un module d'extension C. Parce que seuls des outils standard ont été utilisés, et qu'aucune bibliothèque système non standard n'est liée lors de la compilation, le processus de compilation est simple.
Tu peux utiliser n'importe quel outil de compilation compatible PEP-517. Comme exemple minimal, tu peux exécuter la commande suivante à la racine du dépôt pour installer le package.
pip install .
Cela compile le module d'extension C et installe le package Python complet dans ton environnement.
Un exemple d'utilisation de cette passe de transpileur personnalisée est :
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()
Le résultat de ceci est :
┌───┐ ░
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