Skip to main content
IBM Quantum Platform

Erweitern Sie Qiskit in Python mit C

Die Qiskit-C-API kann in Erweiterungsmodulen für „ Python “ verwendet werden. Sie können leistungskritische Abschnitte Ihrer Qiskit-Erweiterungen in C schreiben, um sie zu beschleunigen, und diese dann sicher an Ihre Nutzer weitergeben.

Dieser Leitfaden führt Sie durch den Prozess der Definition eines vollständigen Erweiterungsmoduls, der Konfiguration seines Build-Prozesses und der Bereitstellung für Benutzer von Python. Das Paket enthält eine einfache Portierung der AddSpectatorMeasures Qiskit-Add-ons in C. Dies ist ein echter benutzerdefinierter Pass mit einem konkreten Anwendungsfall in den Qiskit-Add-ons.

Tip

Die folgenden externen Ressourcen könnten für Sie hilfreich sein:

Die Qiskit-C-API wird für Erweiterungsmodule von „ Python “ auf sehr ähnliche Weise bereitgestellt wie die C-API von „ NumPy “ . Wenn Sie bereits eine Erweiterung für „ NumPy “ programmiert haben, wird Ihnen der Qiskit-Prozess bekannt vorkommen.

Warning

Die Qiskit-C-API befindet sich noch im Versuchsstadium. Daher gibt es noch keine vollständig stabile Programmier- oder Binärschnittstelle, und es kann zwischen den Nebenversionen zu inkompatiblen Änderungen kommen.

Beispielsweise funktioniert ein Erweiterungsmodul, das bei der Erstellung die Version Qiskit v2.4.0 verwendet, garantiert mit der Version Qiskit v2.4.1 zur Laufzeit, könnte jedoch bei Verwendung von Qiskit v2.5.0 zur Laufzeit zu Fehlern führen.


Anforderungen

Befolgen Sie die Anweisungen im Abschnitt „Installation“, um Folgendes zu installieren:

  • Die Standard-C-Compiler-Toolchain für Ihre Plattform
  • Eine Version von „ Python “, die die C-API-Header enthält.

Außerdem sollten Sie mit den in der Qiskit-C-API verfügbaren Funktionen und Objekten vertraut sein oder bereit sein, diese nachzuschlagen, und über gewisse Kenntnisse in der Programmierung mit C verfügen.

Beginnen Sie mit einem leeren Verzeichnis.


Erstellen Sie die Verzeichnisstruktur

Wir werden eine Verzeichnisstruktur src auf Basis von sowie ein einfaches Build-System setuptools auf Basis von verwenden. Diese Anleitung lässt sich leicht an jedes Build-System anpassen, das Erweiterungsmodule erstellen kann.

Die endgültige Struktur wird wie folgt aussehen:

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

Zusammenfassung:

  • pyproject.toml definiert die standardmäßigen statischen Metadaten zum von uns erstellten „ Python “-Paket, einschließlich dessen Name, Autor sowie der Abhängigkeiten für die Erstellungs- und Laufzeit.
  • setup.py enthält die minimale dynamische Konfiguration, die wir zum Erstellen unseres Erweiterungsmoduls benötigen.
  • src/spectator_measures/__init__.py definiert die Benutzeroberfläche und stellt Code bereit, um eine Schnittstelle zu den Komponenten des „ Python “-Raums von Qiskit herzustellen.
  • src/spectator_measures/_coremodule.c definiert das C-Erweiterungsmodul, das den gesamten leistungsrelevanten Code unseres Pakets enthalten wird.

Wir werden jede Datei im Detail prüfen und das Paket mit seinem Erweiterungsmodul zusammenstellen.


Paket-Metadaten definieren

Beginnen Sie damit, die pyproject.toml Datei zu definieren. Dies ist Standard für ein setuptools-basiertes Projekt, wobei eine zusätzliche qiskit Anforderung im build-system.requires Array darstellt, 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"}

Stellen Sie die Laufzeitversion von Qiskit so project.dependencies ein, dass sie mit der bei der Erstellung verwendeten Nebenversion übereinstimmt.

Bei vielen rein auf Python setuptoolsbasierenden Projekten würde es ausreichen, die pyproject.toml Datei zu haben. Unser Modul benötigt jedoch während des Erstellungsvorgangs Zugriff auf die Header-Dateien der Qiskit-C-API. Ab der Version „ v2.4 “ sind diese in den Distributionen „ Qiskit SDK “ und „ Python “ enthalten. Um das Verzeichnis zu finden, in dem sie gespeichert sind, führen Sie folgenden Befehl aus qiskit.capi.get_include(): Das Ergebnis ist eine setup.py Datei, die wie folgt 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 definiert pyproject.toml, und setuptools.setup() wird diese Datei ebenfalls lesen.

Tip

Weitere Informationen zur Konfiguration von setuptools-basierten Projekten finden Sie im setuptoolsBenutzerhandbuch.


Schreiben Sie den Wrapper für den „ Python “-Raum

Technisch gesehen ist es möglich, alles in einer „ Python “-Erweiterung aus C zu definieren. In der Praxis ist es einfacher, von Python aus mit anderem Code im Bereich „ Python “ zu interagieren.

Dieses Paket definiert einen benutzerdefinierten Transpiler-Durchlauf, der von der Klasse „ qiskit.transpiler.TransformationPassPython -space“ abgeleitet ist, jedoch für seine gesamte Geschäftslogik eine Funktion aus dem C-Erweiterungsmodul 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 Einzelheiten dieses Passes sind für diese Anleitung nicht von Bedeutung. Wenn Sie Interesse haben, können Sie die AddSpectatorMeasures API-Dokumentation in qiskit-addon-utils. Dieser Leitfaden enthält eine einfache Portierung dieses Durchlaufs, ohne Unterstützung für Operationen zur Steuerung des Programmablaufs.


Schreiben Sie das C-Erweiterungsmodul

In diesem Abschnitt geht es um die eigentliche C-Erweiterung. Dies ist die komplexeste Datei im Projekt, daher werden wir sie in einzelne Abschnitte unterteilen.

Konfigurieren Sie die Header-Dateien

Wenn Sie ein Erweiterungsmodul für „ Python “ erstellen, müssen Sie diese Datei vor allen Python.h anderen Dateien einbinden. Um die Qiskit-C-API in einem Erweiterungsmodul zu verwenden, müssen Sie das Makro definieren QISKIT_PYTHON_EXTENSION , bevor Sie es einbinden qiskit.h.

Unsere „includes“ sehen dann wie folgt 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>

Schreiben Sie den reinen C-API-Code

Schreiben Sie anschließend die gesamte Geschäftslogik als reinen Qiskit-C-API-Code. Im folgenden Abschnitt werden wir diese Logik auf den Bereich „ Python “ anwenden.

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

  • QkDag *, was dem Raum DAGCircuitPython entspricht.
  • QkTarget *, was dem Raum TargetPython entspricht.
  • QkNeighbors, ein nativer C-API-Typ, der Kopplungsbedingungen für zwei Qubits darstellt.
  • QkCircuitInstruction, ein nativer C-API-Typ zur Abfrage einzelner Befehle.

Die ersten beiden sind Teil unserer Interaktion mit dem „ Python “-Namensraum, doch bei der Arbeit mit ihnen müssen wir lediglich die reine C-API berücksichtigen. In diesem Code findet keine Interaktion mit dem Interpreter von „ Python “ statt.

Beachten Sie, dass alle in diesem Abschnitt definierten Funktionen und Symbole mit static Linkage deklariert sind. Das liegt daran, dass der Interpreter von „ Python “ keine Verknüpfung zu diesem Erweiterungsmodul herstellt; im nächsten Abschnitt werden wir dem Interpreter Informationen zu den verfügbaren Funktionen bereitstellen.

Wir werden nicht näher auf die algorithmischen Details dieses Codes eingehen; es ist zwar lehrreich, für die Demonstration einen aussagekräftigen Transpiler-Durchlauf zu verwenden, doch die genaue Umsetzung des Algorithmus ist für diesen Leitfaden nicht von Bedeutung.

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

Schreiben Sie den Code für die Interaktion mit „ Python “

Die gesamte Geschäftslogik ist nun in reinem C definiert. Als Nächstes muss es sicher mit Python verbunden werden.

Definieren Sie zunächst die einzige Funktion, die für „ Python “ verfügbar sein soll. Dies muss einer festgelegten Signatur entsprechen, die ausschließlich aus „ Python “-Typen besteht, die wie eine fn(self, *args, **kwargs) Methode aussehen. Wir müssen einen PyObject *zurückgeben, der die generische Form eines beliebigen „ Python “-Objekts darstellt.

Die vollständige Funktion sieht wie folgt 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 Argumente vom Typ Python zu akzeptieren.
  2. Definiert Speicherplatz für C-native Objekte, die aus den Argumenten von „ Python “ extrahiert wurden.
  3. Ruft eine Parsing-Funktion auf, um die C-nativen Objekte zu extrahieren, konfiguriert mit der Liste der erwarteten Argumente, Schlüsselwortargumente und den Funktionen, die zu ihrer Konvertierung verwendet werden sollen. Sollte dies fehlschlagen, gibt die Funktion den Fehler weiter.
  4. Verweise auf die C-native Geschäftslogik aus dem vorigen Abschnitt, die den DAG direkt ändert.
  5. Gibt das Objekt „ NonePython -space“ zurück.

Die komplexeste Logik steckt ganz darin PyArg_ParseTupleAndKeywords. Dies ist ausführlich in der CPython-Dokumentation zum Parsen von Argumenten beschrieben, die Sie für weitere Informationen konsultieren sollten.

Die Qiskit-C-API bietet mehrere Funktionen mit Namen wie qk_*_convert_from_python, die als „Konverter“-Funktionen für die Verwendung mit PyArg_Parse*Funktionen konzipiert sind. Diese entsprechen den O& Schlüsseln in der Formatzeichenfolge; hier haben wir und qk_dag_convert_from_pythonverwendetqk_target_convert_from_python. Diese Funktionen übernehmen das C-native Objekt aus dem Argument „Python “, von dem sie abgeleitet sind. Das bedeutet, dass sich Mutationen auf den „ Python “-Bereich auswirken, aber auch, dass Sie darauf achten sollten, den Verweis auf das zugrunde liegende „ Python “-Objekt nicht freizugeben, während Sie das Ergebnis verwenden. Dies ist Standard bei der Programmierung mit der C-API von „ Python “.

Als Nächstes definieren wir die Informationen zu diesem Modul und der darin enthaltenen Funktion, damit wir sie an den „ Python “-Bereich ü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 die Struktur der Moduldefinition werden in der CPython-Dokumentation zur Modulinitialisierung ausführlicher beschrieben.

Geben Sie abschließend unter „ Python “ an, wie das Modul initialisiert werden soll. Dies ist die einzige Funktion in der C-Datei, die exportiert wird. Sein Name muss genau mit dem Muster übereinstimmen PyInit_<mod>, wobei der (unqualifizierte <mod> ) Modulname ist. In diesem Fall lautet der voll qualifizierte spectator_measures._core Modulname _core und der unqualifizierte Name, daher muss unsere Funktion mit dem doppelten Unterstrich aufgerufen werden PyInit__core.

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``PyModuleDef_Init und sind beides Standard Python r C-API-Programmierung. Die Qiskit-spezifische Komponente ist qk_import(). Es ist unbedingt erforderlich, dass Sie diese Funktion innerhalb der Initialisierungsfunktion Ihres Moduls aufrufen; Sie können keine Qiskit-C-API-Funktionen aufrufen, bevor diese nicht erfolgreich ausgeführt wurde.


Verwenden Sie das Paket von Python

Dies ist nun ein vollständiges Paket, das auch ein C-Erweiterungsmodul enthält. Da ausschließlich Standard-Tools verwendet wurden und beim Erstellen keine nicht standardmäßigen Systembibliotheken eingebunden werden, ist der Erstellungsvorgang unkompliziert.

Sie können jedes beliebige Build-Tool von PEP-517-compatible verwenden. Als einfaches Beispiel können Sie den folgenden Befehl im Stammverzeichnis des Repositorys ausführen, um das Paket zu installieren.

pip install .

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

Ein Anwendungsbeispiel für diesen benutzerdefinierten Transpiler-Durchlauf 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 lautet:

        ┌───┐ ░
   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
War diese Seite hilfreich?
Melden Sie auf GitHub einen Fehler, einen Tippfehler oder fordern Sie Inhalte an.