Estendere Qiskit in Python con C
L'API C di Qiskit può essere utilizzata all'interno dei moduli di estensione dell' Python. È possibile scrivere in C le sezioni delle estensioni Qiskit che sono fondamentali per le prestazioni, al fine di accelerarle, e distribuirle poi in tutta sicurezza agli utenti.
Questa guida illustra passo dopo passo come definire un modulo di estensione completo, configurarne
il processo di compilazione e renderlo disponibile agli utenti di Python. Il pacchetto offre un semplice porting
degliAddSpectatorMeasures add-on di Qiskit in C. Si tratta di un vero e proprio pass personalizzato
con un caso d'uso reale negli add-on di Qiskit.
Potresti trovare utili le seguenti risorse esterne:
- La documentazione di CPython sulla creazione di moduli di estensione.
- La documentazione dell' NumPy e sull'uso della sua API in C.
L'API C di Qiskit è resa disponibile per i moduli di estensione dell' Python in modo molto simile all'API C di NumPy Se avete già programmato un'estensione per l' NumPy, il processo di Qiskit vi risulterà familiare.
L'API C di Qiskit è ancora in fase sperimentale. Pertanto, non esiste ancora un'interfaccia di programmazione o binaria completamente stabile, e potrebbero verificarsi modifiche che comportano l'incompatibilità tra le versioni minori.
Ad esempio, un modulo di estensione che utilizza Qiskit v2.4.0 in fase di compilazione è garantito funzionare con Qiskit v2.4.1 in fase di esecuzione, ma potrebbe non funzionare correttamente se si utilizza Qiskit v2.5.0 in fase di esecuzione.
Requisiti
Seguire le istruzioni riportate nella sezione "Installazione" per installare quanto segue:
- La suite di strumenti di compilazione C standard per la tua piattaforma
- Una versione di Python che include le intestazioni dell'API C.
Dovresti inoltre conoscere, o essere disposto a consultare, le funzioni e gli oggetti disponibili nell'API C di Qiskit, e dovresti avere una certa familiarità con la programmazione in C.
Inizia da una directory vuota.
Creare la struttura delle directory
Utilizzeremo una struttura di directory src basata su e un sistema di compilazione semplice setuptoolsbasato su. Queste
istruzioni dovrebbero essere facilmente adattabili a qualsiasi sistema di compilazione in grado di compilare
moduli di estensione.
La struttura finale sarà la seguente:
extension-module
├── pyproject.toml
├── setup.py
└── src
└── spectator_measures
├── __init__.py
└── _coremodule.cIn sintesi:
pyproject.tomldefinisce i metadati statici standard relativi al pacchetto Python che stiamo creando, compresi il nome, l'autore e le dipendenze di compilazione e di esecuzione.setup.pycontiene la configurazione dinamica minima necessaria per compilare il nostro modulo di estensione.src/spectator_measures/__init__.pydefinisce l'interfaccia utente e fornisce del codice per interagire con i componenti dello spazio " Python " 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, creando il pacchetto con il relativo modulo di estensione.
Definire i metadati del pacchetto
Inizia definendo il pyproject.toml file. Questo è lo standard per un setuptoolsprogetto basato su
, sebbene qiskit sia un requisito aggiuntivo build-system.requires nell'array,
oltre a setuptools.
[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"}Imposta la versione runtime di Qiskit in
project.dependencies modo che corrisponda alla versione secondaria utilizzata in fase di compilazione.
In molti progetti basati esclusivamente su setuptoolsPython, sarebbe sufficiente disporre del
pyproject.toml file. Tuttavia, il nostro modulo necessita dell'accesso ai file di intestazione dell'API C di Qiskit durante
il processo di compilazione. A partire dalla versione v2.4, questi sono inclusi nelle distribuzioni Qiskit SDK e Python.
Per individuare la cartella che li contiene, esegui qiskit.capi.get_include().
Il risultato è un setup.py file simile al seguente:
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 relative al pacchetto è definita in pyproject.toml, e setuptools.setup() leggerà
anche quel file.
Per ulteriori
informazioni sulla configurazione dei setuptoolsprogetti basati su [nome], consultare la Guidasetuptools per l'utente.
Scrivere il wrapper " Python -space"
È tecnicamente possibile definire ogni elemento dell'estensione Python utilizzando il linguaggio C. In pratica, è più facile interagire con il codice di altri spazi " Python " direttamente da Python.
Questo pacchetto definisce un passaggio di transpiler personalizzato che deriva dalla classe Pythonqiskit.transpiler.TransformationPass -space, ma utilizza una funzione del modulo di estensione C per
tutta la sua logica di business. Appare così:
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 dagI dettagli precisi di questo pass non sono rilevanti ai fini della presente guida. Se sei interessato, puoi
consultare il AddSpectatorMeasures Documentazione API in
qiskit-addon-utils. Questa guida fornisce una semplice implementazione di quel passaggio,
senza supporto per le operazioni relative al flusso di controllo.
Scrivere il modulo di estensione C
Potresti trovare utili le seguenti risorse:
- La documentazione di CPython sulla creazione di moduli di estensione.
- La documentazione dell' NumPy e sull'uso della sua API in C.
- [Riferimento all'API C] c-apidi Qiskit.
Questa sezione riguarda l'estensione C vera e propria. Questo è il file più complesso del progetto, quindi lo suddivideremo in fasi.
Configurare i file di intestazione
Quando si crea un modulo di estensione per Python, è necessario includere Python.h prima di qualsiasi altro file.
Per utilizzare l'API C di Qiskit in un modulo di estensione, è necessario definire la macro
QISKIT_PYTHON_EXTENSION prima di includerla qiskit.h.
I nostri include assumono quindi la seguente forma:
#define QISKIT_PYTHON_EXTENSION
#include <Python.h>
#include <qiskit.h>
#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>Scrivi il codice API in C puro
Successivamente, scrivi tutta la logica di business come codice API C di Qiskit puro. Nella sezione seguente illustreremo questa logica in un contesto d Python.
Questa sezione contiene esclusivamente codice API C di Qiskit. Utilizza i tipi dell'API C:
QkDag *, corrispondente allo spazio di PythonDAGCircuit.QkTarget *, corrispondente allo spazio di PythonTarget.QkNeighbors, un tipo dell'API C nativa che rappresenta i vincoli di accoppiamento a due qubit.QkCircuitInstruction, un tipo API C nativo per l'interrogazione di singole istruzioni.
I primi due fanno parte della nostra interazione con uno spazio « Python », ma quando li utilizziamo, dobbiamo considerare solo l'API C pura. In questo codice non vi è alcuna interazione con l'interprete di Python .
Si noti che tutte le funzioni e i simboli definiti in questa sezione sono dichiarati con static collegamento.
Questo perché l'interprete di Python non si collegherà a questo modulo di estensione; nella sezione successiva forniremo
all'interprete i dettagli delle funzioni disponibili.
Non ci soffermeremo sui dettagli algoritmici di questo codice; è utile ricorrere a un passaggio del transpiler significativo ai fini della dimostrazione, ma l'implementazione precisa dell'algoritmo non è rilevante ai fini di questa guida.
/**
* 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 per l'interazione con l' Python
Tutta la logica di business è ora definita in C puro. Successivamente, deve essere esposto in modo sicuro a un Python e.
Per cominciare, definisci l'unica funzione che verrà resa disponibile all' Python. Questo deve
rispettare una firma definita, che consiste esclusivamente in tipi di tipo " Python " che assomigliano a un
fn(self, *args, **kwargs) metodo. Dobbiamo restituire un PyObject *, che è la forma generica di
qualsiasi oggetto di tipo Python.
La funzione completa è la seguente:
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 arbitrari di tipo
Python. - Definisce lo spazio necessario per memorizzare gli oggetti nativi in C ricavati dagli argomenti dell' Python.
- Chiama una funzione di analisi sintattica per estrarre gli oggetti nativi in C, configurata con l'elenco degli argomenti previsti, gli argomenti chiave e le funzioni da utilizzare per convertirli. Se l'operazione non va a buon fine, la funzione propaga l'errore.
- Deleghe alla logica di business C-native descritta nella sezione precedente, che modifica il DAG direttamente sul posto.
- Restituisce l'oggetto " Python
None-space".
Tutta la logica più complessa è racchiusa al suo interno PyArg_ParseTupleAndKeywords. Questo è ampiamente documentato nella
documentazione di CPython sull'analisi degli argomenti, che
ti consiglio di consultare per ulteriori informazioni.
L'API C di Qiskit fornisce diverse funzioni con nomi come qk_*_convert_from_python, che sono
concepite come funzioni di "conversione" da utilizzare con PyArg_Parse*le funzioni. Questi corrispondono ai
O& caratteri nella stringa di formato; in questo caso, 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 derivano. Ciò significa che le mutazioni si propagheranno allo spazio d Python, ma anche che
è necessario fare attenzione a non liberare il riferimento all'oggetto d' Python e che le supporta, mentre si utilizza
il risultato. Questo è uno standard nella programmazione tramite l'API C di Python.
Successivamente, definiamo le informazioni relative a questo modulo e alla funzione in esso contenuta, in modo da poterlo passare a uno spazio d Python :
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,
};La tabella dei metodi e la struttura di definizione dei moduli sono descritte in modo più dettagliato nella documentazione di CPython relativa all'inizializzazione dei moduli.
Infine, indica a Python come inizializzare il modulo. Questa è l'unica funzione del file C
che viene esportata. Il suo nome deve corrispondere esattamente allo schema
PyInit_<mod>, dove <mod> è il nome del modulo (senza qualificatori). In questo caso, il nome completo
del modulo è spectator_measures._core, mentre il nome abbreviato è _core, quindi la nostra
funzione deve essere chiamata PyInit__core, con il doppio trattino basso.
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 PyMODINIT_FUNC``PyModuleDef_Init simboli e sono entrambi standard nell'API C di Python. Il
componente specifico di Qiskit è qk_import(). È fondamentale chiamare questa funzione durante la
funzione di inizializzazione del modulo; non sarà possibile chiamare alcuna funzione dell'API C di Qiskit
finché questa non sarà stata eseguita correttamente.
Utilizza il pacchetto disponibile all'indirizzo Python
Ora si tratta di un pacchetto completo, che include un modulo di estensione C. Poiché sono stati utilizzati solo strumenti standard e durante la compilazione non vengono collegate librerie di sistema non standard, il processo di compilazione è semplice.
È possibile utilizzare qualsiasi strumento di compilazione PEP-517-compatible. Per fare un esempio semplice, puoi eseguire il seguente comando nella directory principale del repository per installare il pacchetto.
pip install .Questo comando compila il modulo di estensione C e installa il pacchetto completo Python nel tuo ambiente.
Un esempio di utilizzo di questo passaggio personalizzato del transpiler è:
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 è il seguente:
┌───┐ ░
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