Vai al contenuto principale

Estendere Qiskit in Python con C

L'API C di Qiskit puΓ² essere usata all'interno di moduli di estensione Python. Puoi scrivere le sezioni critiche per le prestazioni delle tue estensioni Qiskit in C per accelerarle, e poi distribuirle in modo sicuro ai tuoi utenti.

Questa guida ti accompagna nel processo di definizione di un modulo di estensione completo, nella configurazione del suo processo di build e nell'esposizione ai tuoi utenti Python. Il pacchetto fornisce un semplice port di AddSpectatorMeasures dagli addon Qiskit in C. Si tratta di un pass personalizzato reale con un caso d'uso reale negli addon Qiskit.

suggerimento

Potresti trovare utili le seguenti risorse esterne:

L'API C di Qiskit Γ¨ esposta per i moduli di estensione Python in modo molto simile alla C API di NumPy. Se hai giΓ  programmato un'estensione NumPy, troverai il processo Qiskit familiare.

avviso

L'API C di Qiskit Γ¨ ancora sperimentale. Pertanto non esiste ancora un'interfaccia di programmazione o binaria completamente stabile, e potrebbero esserci modifiche che rompono la compatibilitΓ  tra versioni minor.

Ad esempio, un modulo di estensione che usa Qiskit v2.4.0 al momento della compilazione Γ¨ garantito per funzionare con Qiskit v2.4.1 a runtime, ma potrebbe non funzionare con Qiskit v2.5.0 a runtime.

Requisiti​

Parti da una directory vuota.

Devi avere disponibile la toolchain standard del compilatore C per la tua piattaforma. Devi anche avere una versione di Python che includa gli header della sua C API (questo Γ¨ standard).

Dovresti avere familiaritΓ , o essere disposto a consultare, le singole funzioni e oggetti disponibili nella Qiskit C API. Dovresti avere una certa familiaritΓ  con la programmazione in C.

Creare la struttura delle directory​

Useremo una struttura di directory basata su src e un semplice sistema di build basato su setuptools. Queste istruzioni dovrebbero essere facili da adattare a qualsiasi sistema di build in grado di costruire moduli di estensione.

La struttura finale sarΓ  simile a questa:

extension-module
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ setup.py
└── src
└── spectator_measures
β”œβ”€β”€ __init__.py
└── _coremodule.c

In sintesi:

  • pyproject.toml definisce i metadati statici standard del pacchetto Python che stiamo creando, inclusi nome, autore e dipendenze di build e runtime.
  • setup.py contiene la configurazione dinamica minima necessaria per compilare il nostro modulo di estensione.
  • src/spectator_measures/__init__.py definisce l'interfaccia rivolta all'utente e fornisce del codice per interfacciarsi con i componenti Python-space di Qiskit.
  • src/spectator_measures/_coremodule.c definisce il modulo di estensione C, che conterrΓ  tutto il codice critico per le prestazioni del nostro pacchetto.

Esamineremo ogni file in dettaglio, costruendo il pacchetto con il suo modulo di estensione.

Definire i metadati del pacchetto​

Inizia definendo il file pyproject.toml. Questo Γ¨ standard per un progetto basato su setuptools, anche se qiskit Γ¨ un requisito aggiuntivo nell'array build-system.requires, in aggiunta a 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"}

A partire da Qiskit v2.4, la C API non Γ¨ ancora stabile al di fuori delle versioni minor (ad esempio, la C API di v2.4.0 sarΓ  compatibile con v2.4.1 ma non con v2.5.0). In futuro, intendiamo estendere questa stabilitΓ  alle versioni major. Per ora, imposta la versione runtime di Qiskit in project.dependencies in modo che corrisponda alla versione minor usata al momento della compilazione.

In molti progetti setuptools pure-Python, sarebbe sufficiente avere il file pyproject.toml. Tuttavia, il nostro modulo necessita di accesso ai file header della C API di Qiskit durante il suo processo di build. A partire dalla v2.4, questi sono inclusi nelle distribuzioni Python dell'SDK di Qiskit. Per individuare la directory che li contiene, esegui qiskit.capi.get_include(). Questo produce un file setup.py che assomiglia a questo:

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 maggior parte delle informazioni del pacchetto Γ¨ definita in pyproject.toml, e setuptools.setup() leggerΓ  anche quel file.

suggerimento

Consulta la Guida utente di setuptools per ulteriori informazioni sulla configurazione di progetti basati su setuptools.

Scrivere il wrapper Python-space​

È tecnicamente possibile definire tutto in un'estensione Python da C. In pratica, è più semplice interagire con altro codice Python-space da Python stesso.

Questo pacchetto definisce un pass di transpiler personalizzato che deriva dalla classe Python-space qiskit.transpiler.TransformationPass, ma usa una funzione del modulo di estensione C per tutta la sua logica di business. Questo appare così:

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

I dettagli esatti di questo pass non sono importanti per questa guida. Se sei interessato, puoi consultare la documentazione API di AddSpectatorMeasures in qiskit-addon-utils. Questa guida produce un semplice port di quel pass, senza supporto per le operazioni di control-flow.

Scrivere il modulo di estensione C​

Questa sezione riguarda la vera estensione C. È il file più complesso del progetto, quindi lo divideremo in fasi.

Configurare i file header​

Quando si compila un modulo di estensione Python, devi includere Python.h prima di qualsiasi altro file. Per usare la C API di Qiskit in un modulo di estensione, devi definire la macro QISKIT_PYTHON_EXTENSION prima di includere qiskit.h.

I nostri include appaiono quindi così:

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>

Scrivere il codice puro della C API​

Successivamente, scrivi tutta la logica di business come codice puro della C API di Qiskit. Esporremo questa logica a Python-space nella sezione seguente.

Questa sezione contiene solo codice puro della C API di Qiskit. Usa i tipi della C API:

  • QkDag *, corrispondente al DAGCircuit in Python-space.
  • QkTarget *, corrispondente al Target in Python-space.
  • QkNeighbors, un tipo nativo della C API che rappresenta i vincoli di accoppiamento a due qubit.
  • QkCircuitInstruction, un tipo nativo della C API per interrogare le singole istruzioni.

I primi due fanno parte della nostra interazione con Python-space, ma quando lavoriamo con loro, dobbiamo considerare solo la C API pura. Non c'Γ¨ interazione con l'interprete Python in questo codice.

Nota che tutte le funzioni e i simboli definiti in questa sezione sono dichiarati con collegamento static. Questo perchΓ© l'interprete Python non si collegherΓ  a questo modulo di estensione; forniremo all'interprete i dettagli delle funzioni disponibili nella sezione successiva.

Non ci soffermeremo sui dettagli algoritmici di questo codice; Γ¨ istruttivo usare un pass di transpiler significativo per la dimostrazione, ma l'implementazione precisa dell'algoritmo non Γ¨ importante per questa guida.

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

Scrivere il codice di interazione con Python​

Tutta la logica di business Γ¨ ora definita in C puro. Successivamente, deve essere esposta in modo sicuro a Python.

Per iniziare, definisci l'unica funzione che verrΓ  esposta a Python. Questa deve seguire una firma definita, che Γ¨ puramente in termini di tipi Python che assomigliano a un metodo fn(self, *args, **kwargs). Dobbiamo restituire un PyObject *, che Γ¨ la forma generica di qualsiasi oggetto Python.

La funzione completa appare così:

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

In breve, la funzione:

  1. Segue una firma definita per accettare argomenti Python arbitrari.
  2. Definisce lo spazio per memorizzare gli oggetti nativi C estratti dagli argomenti Python.
  3. Chiama una funzione di parsing per estrarre gli oggetti nativi C, configurata con l'elenco degli argomenti attesi, degli argomenti keyword e delle funzioni da usare per convertirli. Se fallisce, la funzione propaga l'errore.
  4. Delega alla logica di business nativa in C della sezione precedente, che muta il DAG in-place.
  5. Restituisce l'oggetto Python-space None.

La logica piΓΉ complessa Γ¨ tutta all'interno di PyArg_ParseTupleAndKeywords. Questo Γ¨ ben documentato nella documentazione CPython sul parsing degli argomenti, che dovresti consultare per ulteriori informazioni.

La C API di Qiskit fornisce diverse funzioni con nomi come qk_*_convert_from_python, progettate come funzioni "converter" per l'uso con le funzioni PyArg_Parse*. Corrispondono alle chiavi O& nella stringa di formato; qui abbiamo usato qk_dag_convert_from_python e qk_target_convert_from_python. Queste funzioni prendono in prestito l'oggetto nativo C dall'argomento Python da cui sono derivate. CiΓ² significa che le mutazioni si propagheranno a Python-space, ma anche che devi fare attenzione a non rilasciare il riferimento all'oggetto Python che le supporta mentre le usi. Questo Γ¨ standard nella programmazione della C API di Python.

Successivamente, definiamo le informazioni su questo modulo e sulla funzione che contiene, in modo da poterle passare a 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,
};

Questa tabella dei metodi e struttura di definizione del modulo sono descritte in maggior dettaglio nella documentazione CPython sull'inizializzazione del modulo.

Infine, indica a Python come inizializzare il modulo. Questa Γ¨ l'unica funzione nel file C che viene esportata. Il suo nome deve corrispondere esattamente al pattern PyInit_<mod>, dove <mod> Γ¨ il nome (non qualificato) del modulo. In questo caso, il nome qualificato del modulo Γ¨ spectator_measures._core, e il nome non qualificato Γ¨ _core, quindi la nostra funzione deve chiamarsi PyInit__core, con il doppio underscore.

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

I simboli PyMODINIT_FUNC e PyModuleDef_Init sono entrambi standard della programmazione della C API di Python. Il componente specifico di Qiskit è qk_import(). È fondamentale chiamare questa funzione durante la funzione di inizializzazione del tuo modulo; non potrai chiamare nessuna funzione della C API di Qiskit finché questa non sarà stata eseguita con successo.

Usare il pacchetto da Python​

Questo Γ¨ ora un pacchetto completo, incluso un modulo di estensione C. PoichΓ© sono stati usati solo strumenti standard e nessuna libreria di sistema non standard Γ¨ collegata durante la compilazione, il processo di build Γ¨ semplice.

Puoi usare qualsiasi strumento di build compatibile con PEP-517. Come esempio minimale, puoi eseguire il seguente comando nella root del repository per installare il pacchetto.

pip install .

Questo compila il modulo di estensione C e installa il pacchetto Python completo nel tuo ambiente.

Un esempio d'uso di questo pass di transpiler personalizzato Γ¨:

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 risultato Γ¨:

        β”Œβ”€β”€β”€β” β–‘
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