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.
Potresti trovare utili le seguenti risorse esterne:
- La documentazione CPython sulla scrittura di moduli di estensione.
- La documentazione NumPy sull'uso della sua C API.
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.
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.tomldefinisce i metadati statici standard del pacchetto Python che stiamo creando, inclusi nome, autore e dipendenze di build e runtime.setup.pycontiene la configurazione dinamica minima necessaria per compilare il nostro modulo di estensione.src/spectator_measures/__init__.pydefinisce l'interfaccia rivolta all'utente e fornisce del codice per interfacciarsi con i componenti Python-space di Qiskit.src/spectator_measures/_coremodule.cdefinisce 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.
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β
Potresti trovare utili le seguenti risorse:
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 alDAGCircuitin Python-space.QkTarget *, corrispondente alTargetin 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:
- Segue una firma definita per accettare argomenti Python arbitrari.
- Definisce lo spazio per memorizzare gli oggetti nativi C estratti dagli argomenti Python.
- 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.
- Delega alla logica di business nativa in C della sezione precedente, che muta il DAG in-place.
- 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