Saltar al contenido principal

Extender Qiskit en Python con C

La API de C de Qiskit puede usarse dentro de módulos de extensión de Python. Puedes escribir las secciones críticas para el rendimiento de tus extensiones de Qiskit en C para acelerarlas y, a continuación, distribuirlas de forma segura a tus usuarios.

Esta guía te lleva a través del proceso de definir un módulo de extensión completo, configurar su proceso de compilación y exponerlo a los usuarios de Python. El paquete proporciona un port simple de AddSpectatorMeasures de los complementos de Qiskit a C. Se trata de un pase personalizado real con un caso de uso real en los complementos de Qiskit.

consejo

Puede que encuentres útiles los siguientes recursos externos:

La API de C de Qiskit se expone para los módulos de extensión de Python de una forma muy similar a la API de C de NumPy. Si has programado previamente una extensión de NumPy, encontrarás el proceso de Qiskit familiar.

aviso

La API de C de Qiskit es todavía experimental. Por ello, aún no existe una interfaz de programación o binaria completamente estable, y puede haber cambios que rompan la compatibilidad entre versiones menores.

Por ejemplo, un módulo de extensión que use Qiskit v2.4.0 en tiempo de compilación tiene garantía de funcionar con Qiskit v2.4.1 en tiempo de ejecución, pero podría fallar al usar Qiskit v2.5.0 en tiempo de ejecución.

Requisitos

Comienza desde un directorio vacío.

Debes tener disponible la cadena de herramientas estándar del compilador de C para tu plataforma. También debes tener una versión de Python que incluya sus cabeceras de la API de C (esto es estándar).

Debes estar familiarizado con, o preparado para consultar, las funciones y objetos individuales disponibles en la API de C de Qiskit. Debes tener cierta familiaridad con la programación en C.

Crear la estructura de directorios

Usaremos una estructura de directorios basada en src y un sistema de compilación simple basado en setuptools. Estas instrucciones deberían ser fáciles de adaptar a cualquier sistema de compilación que pueda construir módulos de extensión.

La estructura final tendrá este aspecto:

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

En resumen:

  • pyproject.toml define los metadatos estáticos estándar sobre el paquete Python que estamos creando, incluyendo su nombre, autor y dependencias de compilación y ejecución.
  • setup.py contiene la configuración dinámica mínima que necesitamos para compilar nuestro módulo de extensión.
  • src/spectator_measures/__init__.py define la interfaz de cara al usuario y proporciona algo de código para interactuar con los componentes de Python de Qiskit.
  • src/spectator_measures/_coremodule.c define el módulo de extensión en C, que contendrá todo el código crítico para el rendimiento de nuestro paquete.

Examinaremos cada archivo en detalle, construyendo el paquete con su módulo de extensión.

Definir los metadatos del paquete

Empieza por definir el archivo pyproject.toml. Esto es estándar para un proyecto basado en setuptools, aunque qiskit es un requisito adicional en el array build-system.requires, además de 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 partir de Qiskit v2.4, la API de C aún no es estable fuera de versiones menores (por ejemplo, la API de C para v2.4.0 será compatible con v2.4.1 pero no con v2.5.0). En el futuro, tenemos la intención de ampliar esta estabilidad a las versiones mayores. Por ahora, establece la versión de ejecución de Qiskit en project.dependencies para que coincida con la versión menor usada en tiempo de compilación.

En muchos proyectos de setuptools con código Python puro, sería suficiente con tener el archivo pyproject.toml. Sin embargo, nuestro módulo necesita acceso a los archivos de cabecera de la API de C de Qiskit durante su proceso de compilación. A partir de v2.4, estos se incluyen en las distribuciones Python del SDK de Qiskit. Para localizar el directorio que los contiene, ejecuta qiskit.capi.get_include(). Esto da lugar a un archivo setup.py con el siguiente aspecto:

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 mayor parte de la información del paquete está definida en pyproject.toml, y setuptools.setup() también leerá ese archivo.

consejo

Consulta la Guía de usuario de setuptools para más información sobre cómo configurar proyectos basados en setuptools.

Escribir el envoltorio en Python

Técnicamente es posible definir todo en una extensión de Python desde C. En la práctica, es más fácil interactuar con otro código Python desde el propio Python.

Este paquete define un pase de transpilación personalizado que deriva de la clase Python qiskit.transpiler.TransformationPass, pero usa una función del módulo de extensión en C para toda su lógica de negocio. Tiene el siguiente aspecto:

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

Los detalles exactos de este pase no son importantes para esta guía. Si te interesa, puedes consultar la documentación de la API de AddSpectatorMeasures en qiskit-addon-utils. Esta guía produce un port simple de ese pase, sin soporte para operaciones de control de flujo.

Escribir el módulo de extensión en C

Esta sección trata sobre la extensión de C propiamente dicha. Este es el archivo más complejo del proyecto, por lo que lo dividiremos en etapas.

Configurar los archivos de cabecera

Al compilar un módulo de extensión de Python, debes incluir Python.h antes que cualquier otro archivo. Para usar la API de C de Qiskit en un módulo de extensión, debes definir la macro QISKIT_PYTHON_EXTENSION antes de incluir qiskit.h.

Nuestras inclusiones (includes) tienen entonces el siguiente aspecto:

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>

Escribir el código puro de la API de C

A continuación, escribe toda la lógica de negocio como código puro de la API de C de Qiskit. Expondremos esta lógica al espacio Python en la siguiente sección.

Esta sección contiene únicamente código puro de la API de C de Qiskit. Usa los tipos de la API de C:

  • QkDag *, correspondiente al DAGCircuit del espacio Python.
  • QkTarget *, correspondiente al Target del espacio Python.
  • QkNeighbors, un tipo nativo de la API de C que representa restricciones de acoplamiento entre dos qubits.
  • QkCircuitInstruction, un tipo nativo de la API de C para consultar instrucciones individuales.

Los dos primeros forman parte de nuestra interacción con el espacio Python, pero cuando trabajamos con ellos, solo necesitamos considerar la API de C pura. No hay interacción con el intérprete de Python en este código.

Ten en cuenta que todas las funciones y símbolos definidos en esta sección se declaran con enlace static. Esto se debe a que el intérprete de Python no enlazará (link) contra este módulo de extensión; proporcionaremos al intérprete los detalles de las funciones disponibles en la siguiente sección.

No nos detendremos en los detalles algorítmicos de este código; es instructivo usar un pase de transpilación significativo para la demostración, pero la implementación precisa del algoritmo no es importante para esta guía.

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

Escribir el código de interacción con Python

Toda la lógica de negocio ya está definida en C puro. A continuación, debe exponerse de forma segura a Python.

Para empezar, define la única función que se expondrá a Python. Esta debe seguir una firma definida, que está puramente en términos de tipos Python con el aspecto de un método fn(self, *args, **kwargs). Debemos devolver un PyObject *, que es la forma genérica de cualquier objeto Python.

La función completa tiene el siguiente aspecto:

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

En resumen, la función:

  1. Sigue una firma definida para aceptar argumentos arbitrarios de Python.
  2. Define espacio para almacenar los objetos nativos de C extraídos de los argumentos de Python.
  3. Llama a una función de análisis (parsing) para extraer los objetos nativos de C, configurada con la lista de argumentos esperados, argumentos de palabras clave y las funciones a usar para convertirlos. Si esto falla, la función propaga el error.
  4. Delega en la lógica de negocio nativa de C de la sección anterior, que muta el DAG en su lugar.
  5. Devuelve el objeto Python-space None.

La lógica más compleja está toda dentro de PyArg_ParseTupleAndKeywords. Esto está bien documentado en la documentación de CPython sobre análisis de argumentos, que deberías consultar para más información.

La API de C de Qiskit proporciona varias funciones con nombres como qk_*_convert_from_python, diseñadas como funciones "convertidoras" para usar con las funciones PyArg_Parse*. Estas corresponden a las claves O& en la cadena de formato; aquí usamos qk_dag_convert_from_python y qk_target_convert_from_python. Estas funciones toman prestado el objeto nativo de C del argumento Python del que se derivan. Esto significa que las mutaciones se propagarán al espacio Python, pero también que debes tener cuidado de no liberar tu referencia al objeto Python que las respalda mientras usas el resultado. Esto es estándar en la programación de la API de C de Python.

A continuación, definimos la información sobre este módulo y la función que contiene, para poder pasarla al espacio Python:

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

Esta tabla de métodos y la estructura de definición del módulo se describen con más detalle en la documentación de CPython sobre la tabla de métodos del módulo y la función de inicialización.

Por último, indica a Python cómo inicializar el módulo. Esta es la única función del archivo C que se exporta. Su nombre debe coincidir exactamente con el patrón PyInit_<mod>, donde <mod> es el nombre del módulo (sin cualificar). En este caso, el nombre completo del módulo es spectator_measures._core, y el nombre sin cualificar es _core, por lo que nuestra función debe llamarse PyInit__core, con el doble guion bajo.

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

Los símbolos PyMODINIT_FUNC y PyModuleDef_Init son programación estándar de la API de C de Python. El componente específico de Qiskit es qk_import(). Es fundamental que llames a esta función durante la función de inicialización de tu módulo; no podrás llamar a ninguna función de la API de C de Qiskit hasta que se haya ejecutado correctamente.

Usar el paquete desde Python

Este es ahora un paquete completo, incluyendo un módulo de extensión en C. Como solo se usaron herramientas estándar y no se enlazan bibliotecas del sistema no estándar durante el tiempo de compilación, el proceso de compilación es simple.

Puedes usar cualquier herramienta de compilación compatible con PEP-517. Como ejemplo mínimo, puedes ejecutar el siguiente comando en la raíz del repositorio para instalar el paquete.

pip install .

Esto compila el módulo de extensión en C e instala el paquete Python completo en tu entorno.

Un ejemplo de uso de este pase de transpilación personalizado es:

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

El resultado de esto es:

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