Amplíe Qiskit en Python con C
La API de C de Qiskit se puede utilizar en los módulos de extensión de l Python. Puedes escribir las secciones de tus extensiones de Qiskit que sean críticas para el rendimiento en C para acelerarlas y, luego, distribuirlas de forma segura a tus usuarios.
Esta guía te guía a través del proceso de definir un módulo de extensión completo, configurar
su proceso de compilación y ponerlo a disposición de los usuarios de Python. El paquete ofrece una sencilla adaptación
[deAddSpectatorMeasures los complementos de] AddSpectatorMeasures-code Qiskit a C. Se trata de un paso personalizado real
con un caso de uso real en los complementos de Qiskit.
Quizás te resulten útiles los siguientes recursos externos:
- La documentación de CPython sobre cómo escribir módulos de extensión.
- La documentación de « NumPy » sobre el uso de su API en C.
La API C de Qiskit se pone a disposición de los módulos de extensión de Python de una manera muy similar a la API C de NumPy . Si ya has programado alguna vez una extensión de « NumPy », el proceso de Qiskit te resultará familiar.
La API de C de Qiskit sigue siendo experimental. Por lo tanto, todavía no existe una interfaz de programación o binaria totalmente estable, y puede haber cambios que afecten a la compatibilidad entre versiones secundarias.
Por ejemplo, se garantiza que un módulo de extensión que utilice Qiskit v2.4.0 en el momento de la compilación funcionará con Qiskit v2.4.1 en tiempo de ejecución, pero podría dejar de funcionar si se utiliza Qiskit v2.5.0 en tiempo de ejecución.
Requisitos
Sigue las instrucciones del apartado «Instalación» para instalar lo siguiente:
- El conjunto de herramientas de compilación estándar de C para tu plataforma
- Una versión de « Python » que incluye sus encabezados de la API en C.
También deberías conocer, o estar dispuesto a consultar, las funciones y los objetos disponibles en la API de C de Qiskit, y deberías tener ciertos conocimientos de programación en C.
Empieza con un directorio vacío.
Crea la estructura de directorios
Utilizaremos una estructura de srcdirectorios basada en y un sistema de compilación sencillo setuptoolsbasado en. Estas
instrucciones deberían poder adaptarse fácilmente a cualquier sistema de compilación capaz de compilar
módulos de extensión.
La estructura final tendrá el siguiente aspecto:
extension-module
├── pyproject.toml
├── setup.py
└── src
└── spectator_measures
├── __init__.py
└── _coremodule.cEn resumen:
pyproject.tomldefine los metadatos estáticos estándar del paquete « Python » que estamos creando, incluidos su nombre, autor y dependencias de compilación y de tiempo de ejecución.setup.pycontiene la configuración dinámica mínima que necesitamos para compilar nuestro módulo de extensión.src/spectator_measures/__init__.pydefine la interfaz de usuario y proporciona código para interactuar con los componentes del espacio « Python » de Qiskit.src/spectator_measures/_coremodule.cdefine el módulo de extensión C, que contendrá todo el código crítico para el rendimiento de nuestro paquete.
Analizaremos cada archivo en detalle y crearemos el paquete junto con su módulo de extensión.
Definir los metadatos del paquete
Empieza por definir el pyproject.toml archivo. Esto es habitual en un setuptoolsproyecto basado en
, aunque qiskit es un requisito adicional en la build-system.requires matriz,
además de 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"}Configura la versión de tiempo de ejecución de Qiskit para
project.dependencies que coincida con la versión secundaria utilizada en el momento de la compilación.
En muchos proyectos basados exclusivamente en setuptoolsPython, bastaría con tener el
pyproject.toml archivo. Sin embargo, nuestro módulo necesita acceder a los archivos de encabezado de la API C de Qiskit durante
su proceso de compilación. A partir de la versión v2.4, estos se incluyen en las distribuciones Qiskit SDK y Python.
Para localizar el directorio que los contiene, ejecuta qiskit.capi.get_include().
El resultado es un setup.py archivo que tiene este aspecto:
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 se define en pyproject.toml, y también setuptools.setup()
leerá ese archivo.
Consulte la Guía setuptools del usuario para obtener más
información sobre la configuración de setuptoolsproyectos basados en.
Escribe el contenedor de espacios « Python »
Técnicamente, es posible definir todo en una extensión de Python desde C. En la práctica, resulta más fácil interactuar con otro código del espacio « Python » desde el propio Python.
Este paquete define una etapa de transpilador personalizada que deriva de la clase «
qiskit.transpiler.TransformationPassPython -space», pero utiliza una función del módulo de extensión C para
toda su lógica de negocio. Esto tiene el siguiente aspecto:
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 dagLos detalles concretos de este pase no son relevantes para esta guía. Si te interesa, puedes
consultar el AddSpectatorMeasures Documentación de la API en
qiskit-addon-utils. Esta guía ofrece una adaptación sencilla de ese paso,
sin compatibilidad con operaciones de flujo de control.
Escribe el módulo de extensión en C
Quizás te resulten útiles los siguientes recursos:
Esta sección trata sobre la extensión C propiamente dicha. Este es el archivo más complejo del proyecto, así que lo dividiremos en fases.
Configura los archivos de encabezado
Al crear un módulo de extensión de Python, debes incluirlo Python.h antes que cualquier otro archivo.
Para utilizar la API de C de Qiskit en un módulo de extensión, debes definir la macro
QISKIT_PYTHON_EXTENSION antes de incluirla qiskit.h.
Nuestros «includes» quedan entonces así:
#define QISKIT_PYTHON_EXTENSION
#include <Python.h>
#include <qiskit.h>
#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>Escribe el código de la API en C puro
A continuación, escribe toda la lógica de negocio como código puro de la API C de Qiskit. En la siguiente sección, aplicaremos esta lógica a un espaci Python.
Esta sección contiene únicamente código de la API de C de Qiskit. Utiliza los tipos de la API de C:
QkDag *, correspondiente al espacio de PythonDAGCircuit.QkTarget *, correspondiente al espacio de PythonTarget.QkNeighbors, un tipo de la API nativa de C que representa restricciones de acoplamiento de dos qubits.QkCircuitInstruction, un tipo de la API C nativa para consultar instrucciones individuales.
Las dos primeras forman parte de nuestra interacción con el espacio « Python », pero al trabajar con ellas, solo tenemos que tener en cuenta la API de C pura. En este código no hay ninguna interacción con el intérprete de « Python ».
Tenga en cuenta que todas las funciones y símbolos definidos en esta sección se declaran con static enlace.
Esto se debe a que el intérprete de Python no se vinculará con este módulo de extensión; en la siguiente sección le proporcionaremos
al intérprete los detalles de las funciones disponibles.
No nos detendremos en los detalles algorítmicos de este código; resulta instructivo utilizar una pasada del transpilador significativa para la demostración, pero la implementación concreta del algoritmo no es importante para esta guía.
/**
* 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;
}Escribe el código de interacción de « Python »
Ahora toda la lógica de negocio está definida en C puro. A continuación, hay que exponerlo de forma segura a Python.
Para empezar, define la única función que se expondrá en Python. Esto debe
seguir una firma definida, que se expresa exclusivamente en términos de tipos de « Python » que se asemejan a un
fn(self, *args, **kwargs) método. Tenemos que devolver un PyObject *, que es la forma genérica de
cualquier objeto de tipo Python.
La función completa queda así:
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:
- Sigue una firma definida para aceptar argumentos arbitrarios de tipo
Python. - Define el espacio para almacenar los objetos nativos de C extraídos de los argumentos de « Python ».
- Llama a una función de análisis sintáctico para extraer los objetos nativos de C, configurada con la lista de argumentos esperados, los argumentos clave y las funciones que se deben utilizar para convertirlos. Si esto falla, la función propaga el error.
- Delega en la lógica de negocio nativa de C de la sección anterior, que modifica el DAG in situ.
- Devuelve el objeto « Python
None-space».
Toda la lógica más compleja está ahí dentro PyArg_ParseTupleAndKeywords. Esto queda bien reflejado en la
documentación de CPython sobre el análisis de argumentos, que
deberías consultar para obtener más información.
La API de C de Qiskit ofrece varias funciones con nombres como qk_*_convert_from_python, que están
diseñadas como funciones «convertidoras» para su uso con PyArg_Parse*funciones. Estas corresponden a las
O& claves de la cadena de formato; en este caso, hemos utilizado 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 de « Python », pero también que
debes tener cuidado de no liberar la referencia al objeto « Python » que las respalda mientras utilizas
el resultado. Esto es habitual en la programación de la API C de Python.
A continuación, definimos la información sobre este módulo y la función que contiene, para poder pasarla a un espaci 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,
};Esta tabla de métodos y la estructura de definición de módulos se describen con más detalle en la documentación de CPython sobre la inicialización de módulos.
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 calificaciones). En este caso, el nombre completo
del módulo es spectator_measures._core, y el nombre sin prefijo es _core, por lo que nuestra
función debe llamarse PyInit__core, con el doble guión bajo.
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``PyModuleDef_Init y son estándar en la programación 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 C de Qiskit
hasta que esta se haya ejecutado correctamente.
Utiliza el paquete disponible en Python
Ahora se trata de un paquete completo, que incluye un módulo de extensión en C. Dado que solo se ha utilizado herramientas estándar y no se han vinculado bibliotecas del sistema no estándar durante la compilación, el proceso de compilación es sencillo.
Puedes utilizar cualquier herramienta de compilación de PEP-517-compatible. A modo de ejemplo sencillo, puedes ejecutar el siguiente comando en el directorio raíz del repositorio para instalar el paquete.
pip install .Esto compila el módulo de extensión en C e instala el paquete completo « Python » en tu entorno.
Un ejemplo de uso de esta pasada del transpilador 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 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