Aller au contenu principal

É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.

conseil

Tu pourrais trouver les ressources externes suivantes utiles :

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.

avertissement

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.toml dé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.py contient la configuration dynamique minimale nécessaire pour compiler notre module d'extension.
  • src/spectator_measures/__init__.py définit l'interface utilisateur et fournit du code pour interagir avec les composants Python-space de Qiskit.
  • src/spectator_measures/_coremodule.c dé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.

conseil

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

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 à DAGCircuit dans Python-space.
  • QkTarget *, correspondant à Target dans 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 :

  1. Suit une signature définie pour accepter des arguments Python arbitraires.
  2. Définit de l'espace pour stocker les objets natifs C extraits des arguments Python.
  3. 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.
  4. Délègue à la logique métier native C de la section précédente, qui mute le DAG sur place.
  5. 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