Zum Hauptinhalt springen
Dieses Projekt befindet sich in der Beta-Phase. Einen Fehler gefunden? GitHub Issue erstellen. Frage oder Idee? In Discussions fragen — wir freuen uns über dein Feedback!
Wie ist die Übersetzungsqualität auf dieser Seite?

Qiskit in Python mit C erweitern

Die Qiskit-C-API kann innerhalb von Python-Erweiterungsmodulen genutzt werden. Du kannst leistungskritische Abschnitte deiner Qiskit-Erweiterungen in C schreiben, um sie zu beschleunigen, und sie dann sicher an deine Nutzer weitergeben.

Diese Anleitung führt dich durch den Prozess, ein vollständiges Erweiterungsmodul zu definieren, seinen Build-Prozess zu konfigurieren und es Python-Nutzern zugänglich zu machen. Das Paket bietet eine einfache Portierung von AddSpectatorMeasures aus den Qiskit-Addons nach C. Das ist ein echter benutzerdefinierter Pass mit einem echten Anwendungsfall in den Qiskit-Addons.

tipp

Die folgenden externen Ressourcen könnten dir hilfreich sein:

Die Qiskit-C-API wird für Python-Erweiterungsmodule auf eine sehr ähnliche Weise wie die NumPy-C-API bereitgestellt. Wenn du bereits ein NumPy-Erweiterungsmodul programmiert hast, wird dir der Qiskit-Prozess vertraut vorkommen.

warnung

Die Qiskit-C-API ist noch experimentell. Daher gibt es noch keine vollständig stabile Programmier- oder Binärschnittstelle, und es kann zwischen Minor-Versionen zu Breaking Changes kommen.

Zum Beispiel ist ein Erweiterungsmodul, das zur Build-Zeit Qiskit v2.4.0 verwendet, garantiert mit Qiskit v2.4.1 zur Laufzeit kompatibel, könnte aber beim Einsatz von Qiskit v2.5.0 zur Laufzeit nicht mehr funktionieren.

Voraussetzungen​

Folge den Anweisungen im Thema Installation, um Folgendes zu installieren:

  • Die Standard-C-Compiler-Toolchain für deine Plattform

  • Eine Python-Version, die ihre C-API-Header enthält.

Du solltest außerdem mit den verfügbaren Funktionen und Objekten der Qiskit C API vertraut sein oder bereit sein, sie nachzuschlagen, und solltest über einige Grundkenntnisse der C-Programmierung verfügen.

Starte in einem leeren Verzeichnis.

Verzeichnisstruktur erstellen​

Wir verwenden eine src-basierte Verzeichnisstruktur und ein einfaches setuptools-basiertes Build-System. Diese Anweisungen lassen sich leicht auf jedes Build-System anpassen, das Erweiterungsmodule erstellen kann.

Die finale Struktur sieht so aus:

extension-module
├── pyproject.toml
├── setup.py
└── src
└── spectator_measures
├── __init__.py
└── _coremodule.c

Zusammenfassend:

  • pyproject.toml definiert die standardmäßigen statischen Metadaten über das Python-Paket, das wir erstellen, einschließlich Name, Autor sowie Build- und Laufzeit-Abhängigkeiten.

  • setup.py enthält die minimale dynamische Konfiguration, die wir zum Erstellen unseres Erweiterungsmoduls benötigen.

  • src/spectator_measures/__init__.py definiert die nutzerseitige Schnittstelle und stellt Code bereit, um mit Qiskits Python-seitigen Komponenten zu interagieren.

  • src/spectator_measures/_coremodule.c definiert das C-Erweiterungsmodul, das den gesamten leistungskritischen Code unseres Pakets enthalten wird.

Wir werden jede Datei im Detail untersuchen und das Paket mit seinem Erweiterungsmodul aufbauen.

Paket-Metadaten definieren​

Beginne mit der Definition der Datei pyproject.toml. Das ist bei einem setuptools-basierten Projekt üblich, wobei qiskit eine zusätzliche Anforderung im Array build-system.requires ist, zusätzlich zu 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"}

Lege die Runtime-Version von Qiskit in project.dependencies so fest, dass sie mit der beim Build verwendeten Minor-Version übereinstimmt.

In vielen reinen Python-setuptools-Projekten würde die Datei pyproject.toml ausreichen. Unser Modul benötigt jedoch während seines Build-Prozesses Zugriff auf die Qiskit-C-API-Header-Dateien. Ab v2.4 sind diese in den Qiskit-SDK-Python-Distributionen enthalten. Um das Verzeichnis zu finden, das sie enthält, führe qiskit.capi.get_include() aus. Das ergibt eine setup.py-Datei, die so aussieht:

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])

Die meisten Paketinformationen sind in pyproject.toml definiert, und setuptools.setup() liest diese Datei ebenfalls.

tipp

Weitere Informationen zur Konfiguration von setuptools-basierten Projekten findest du im setuptools User Guide.

Python-seitigen Wrapper schreiben​

Es ist technisch möglich, alles in einer Python-Erweiterung in C zu definieren. In der Praxis ist es einfacher, direkt in Python mit anderem Python-seitigen Code zu interagieren.

Dieses Paket definiert einen benutzerdefinierten Transpiler-Pass, der von der Python-seitigen qiskit.transpiler.TransformationPass-Klasse ableitet, aber eine Funktion aus dem C-Erweiterungsmodul für die gesamte Geschäftslogik verwendet. Das sieht so aus:

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

Die genauen Details dieses Passes sind für diese Anleitung unwesentlich. Wenn du interessiert bist, kannst du die AddSpectatorMeasures-API-Dokumentation in qiskit-addon-utils konsultieren. Diese Anleitung erstellt eine einfache Portierung dieses Passes, ohne Unterstützung für Kontrollfluss-Operationen.

C-Erweiterungsmodul schreiben​

Dieser Abschnitt befasst sich mit der eigentlichen C-Erweiterung. Das ist die komplexeste Datei im Projekt, daher werden wir sie in Phasen aufteilen.

Header-Dateien konfigurieren​

Beim Erstellen eines Python-Erweiterungsmoduls musst du Python.h vor jeder anderen Datei einbinden. Um die Qiskit-C-API in einem Erweiterungsmodul zu verwenden, musst du das Makro QISKIT_PYTHON_EXTENSION definieren, bevor du qiskit.h einbindest.

Unsere Includes sehen dann so aus:

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>

Reinen C-API-Code schreiben​

Als Nächstes schreibe die gesamte Geschäftslogik als reinen Qiskit-C-API-Code. Diese Logik werden wir im folgenden Abschnitt für Python-Space verfügbar machen.

Dieser Abschnitt enthält ausschließlich reinen Qiskit-C-API-Code. Er verwendet die C-API-Typen:

  • QkDag *, entsprechend dem Python-seitigen DAGCircuit.

  • QkTarget *, entsprechend dem Python-seitigen Target.

  • QkNeighbors, ein nativer C-API-Typ zur Darstellung von Zwei-Qubit-Kopplungsbeschränkungen.

  • QkCircuitInstruction, ein nativer C-API-Typ zum Abfragen einzelner Instruktionen.

Die ersten beiden sind Teil unserer Interaktion mit der Python-Seite, aber wenn wir mit ihnen arbeiten, müssen wir nur die reine C-API berücksichtigen. In diesem Code gibt es keine Interaktion mit dem Python-Interpreter.

Beachte, dass alle Funktionen und Symbole, die in diesem Abschnitt definiert werden, mit static-Bindung deklariert sind. Das liegt daran, dass der Python-Interpreter nicht gegen dieses Erweiterungsmodul linkt; wir werden dem Interpreter die Details der verfügbaren Funktionen im nächsten Abschnitt bereitstellen.

Wir werden uns nicht bei den algorithmischen Details dieses Codes aufhalten; es ist lehrreich, einen praxisnahen Transpiler-Pass für die Demonstration zu verwenden, aber die genaue Implementierung des Algorithmus ist für diese Anleitung nicht wichtig.

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;
}

Python-Interaktionscode schreiben​

Die gesamte Geschäftslogik ist nun in reinem C definiert. Als Nächstes muss sie sicher für Python zugänglich gemacht werden.

Definiere zunächst die einzige Funktion, die Python zugänglich gemacht wird. Diese muss einer definierten Signatur folgen, die ausschließlich Python-Typen verwendet und wie eine fn(self, *args, **kwargs)-Methode aussieht. Wir müssen einen PyObject * zurückgeben, die generische Form eines beliebigen Python-Objekts.

Die vollständige Funktion sieht so aus:

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;
}

Kurz gesagt, die Funktion:

  1. Folgt einer definierten Signatur, um beliebige Python-Argumente entgegenzunehmen.

  2. Reserviert Speicher für aus den Python-Argumenten geparste native C-Objekte.

  3. Ruft eine Parse-Funktion auf, um die nativen C-Objekte zu extrahieren, konfiguriert mit der Liste der erwarteten Argumente, Schlüsselwortargumente und der zu verwendenden Konvertierungsfunktionen. Wenn dies fehlschlägt, gibt die Funktion den Fehler weiter.

  4. Delegiert an die native C-Geschäftslogik des vorherigen Abschnitts, die den DAG in-place mutiert.

  5. Gibt das Python-seitige None-Objekt zurück.

Die komplexeste Logik befindet sich vollständig innerhalb von PyArg_ParseTupleAndKeywords. Dies ist in der CPython-Dokumentation zum Parsen von Argumenten gut dokumentiert, die du für weitere Informationen konsultieren solltest.

Die Qiskit-C-API stellt mehrere Funktionen mit Namen wie qk_*_convert_from_python bereit, die als "Konverter"-Funktionen für die Verwendung mit PyArg_Parse*-Funktionen konzipiert sind. Diese entsprechen den O&-Schlüsseln im Formatstring; hier haben wir qk_dag_convert_from_python und qk_target_convert_from_python verwendet. Diese Funktionen leihen das native C-Objekt vom Python-Argument, aus dem sie abgeleitet werden. Das bedeutet, dass Mutationen auf der Python-Seite propagiert werden, aber auch, dass du darauf achten solltest, deine Referenz auf das zugrunde liegende Python-Objekt nicht freizugeben, während du das Ergebnis verwendest. Das ist Standard in der Python-C-API-Programmierung.

Als Nächstes definieren wir die Informationen über dieses Modul und die darin enthaltene Funktion, damit wir sie Python-Space übergeben können:

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,
};

Diese Methodentabelle und Moduldefinitionsstruktur sind in der CPython-Dokumentation zur Modul-Initialisierung ausführlicher beschrieben.

Schließlich teile Python mit, wie das Modul initialisiert werden soll. Das ist die einzige Funktion in der C-Datei, die exportiert wird. Ihr Name muss genau dem Muster PyInit_<mod> entsprechen, wobei <mod> der (unqualifizierte) Modulname ist. In diesem Fall ist der voll qualifizierte Modulname spectator_measures._core, und der unqualifizierte Name ist _core, daher muss unsere Funktion PyInit__core heißen, mit dem doppelten Unterstrich.

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);
}

Die Symbole PyMODINIT_FUNC und PyModuleDef_Init sind beide Standard-Python-C-API-Programmierung. Die Qiskit-spezifische Komponente ist qk_import(). Du musst diese Funktion unbedingt während der Initialisierungsfunktion deines Moduls aufrufen; du wirst keine Qiskit-C-API-Funktionen aufrufen können, bis diese erfolgreich ausgeführt wurde.

Das Paket aus Python verwenden​

Dies ist nun ein vollständiges Paket, einschließlich eines C-Erweiterungsmoduls. Da nur Standard-Tooling verwendet wurde und keine nicht-standardmäßigen Systembibliotheken zur Build-Zeit gelinkt werden, ist der Build-Prozess einfach.

Du kannst ein beliebiges PEP-517-kompatibles Build-Tool verwenden. Als minimales Beispiel kannst du den folgenden Befehl im Repository-Root ausführen, um das Paket zu installieren.

pip install .

Dadurch wird das C-Erweiterungsmodul kompiliert und das vollständige Python-Paket in deiner Umgebung installiert.

Ein Beispiel für die Verwendung dieses benutzerdefinierten Transpiler-Passes ist:

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()

Das Ergebnis davon ist:

┌───┐ ░
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