Перейти до основного вмісту

Розширення Qiskit у Python за допомогою C

Qiskit C API можна використовувати всередині Python-модулів розширень. Ти можеш написати продуктивно-критичні секції своїх розширень Qiskit на C для їх пришвидшення, а потім безпечно поширити їх серед своїх користувачів.

Цей посібник проведе тебе через процес визначення повного модуля розширення, налаштування процесу його збірки та надання доступу до нього для Python-користувачів. Пакет надає простий порт AddSpectatorMeasures з доповнень Qiskit на C. Це справжній кастомний прохід з реальним варіантом використання в доповненнях Qiskit.

порада

Можливо, тобі стануть у пригоді такі зовнішні ресурси:

Qiskit C API надається для Python-модулів розширень дуже схожим чином до NumPy C API. Якщо ти раніше програмував NumPy-розширення, процес Qiskit видасться тобі знайомим.

попередження

Qiskit C API досі є експериментальним. Тому стабільний програмний або бінарний інтерфейс ще не зафіксовано, і між мінорними версіями можуть бути несумісні зміни.

Наприклад, модуль розширення, що використовує Qiskit v2.4.0 під час збірки, гарантовано працюватиме з Qiskit v2.4.1 під час виконання, але може не працювати з Qiskit v2.5.0 під час виконання.

Вимоги

Почни з чистої директорії.

На твоїй платформі має бути доступний стандартний набір інструментів компілятора C. Також потрібна версія Python, яка включає заголовні файли її C API (це стандартно).

Тобі слід бути знайомим, або бути готовим звернутися до документації, з окремими функціями та об'єктами, доступними в Qiskit C API. Також потрібні базові знання програмування на C.

Створення структури директорій

Ми використаємо структуру директорій на основі src і просту систему збірки на основі setuptools. Ці інструкції легко адаптувати до будь-якої системи збірки, яка може збирати модулі розширень.

Кінцева структура матиме вигляд:

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

Підсумовуючи:

  • pyproject.toml визначає стандартні статичні метадані про Python-пакет, що створюється, включно з його назвою, автором, а також залежностями під час збірки та виконання.
  • setup.py містить мінімальну динамічну конфігурацію, потрібну для збірки нашого модуля розширення.
  • src/spectator_measures/__init__.py визначає інтерфейс для користувача та надає код для взаємодії з Python-компонентами Qiskit.
  • src/spectator_measures/_coremodule.c визначає C-модуль розширення, який міститиме весь продуктивно-критичний код нашого пакету.

Ми детально розглянемо кожен файл, поступово будуючи пакет з його модулем розширення.

Визначення метаданих пакету

Почни з визначення файлу pyproject.toml. Це стандартно для проекту на основі setuptools, хоча qiskit є додатковою вимогою в масиві build-system.requires, на додаток до 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"}

Станом на Qiskit v2.4, C API ще не є стабільним поза мінорними версіями (наприклад, C API для v2.4.0 буде сумісним з v2.4.1, але не з v2.5.0). У майбутньому ми плануємо розширити цю стабільність до мажорних версій. Наразі встанови версію Qiskit під час виконання в project.dependencies відповідно до мінорної версії, що використовується під час збірки.

У багатьох чистих Python-проектах на основі setuptools достатньо мати файл pyproject.toml. Проте нашому модулю потрібен доступ до заголовних файлів Qiskit C API під час збірки. Починаючи з v2.4, вони включені в Python-дистрибутиви Qiskit SDK. Щоб знайти директорію, що їх містить, виконай qiskit.capi.get_include(). У результаті файл setup.py матиме вигляд:

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

Більша частина інформації про пакет визначена в pyproject.toml, і setuptools.setup() також читатиме цей файл.

порада

Дивись Посібник користувача setuptools для отримання додаткової інформації щодо налаштування проектів на основі setuptools.

Написання Python-обгортки

Технічно можливо визначити все в Python-розширенні на C. На практиці простіше взаємодіяти з іншим Python-кодом безпосередньо з Python.

Цей пакет визначає кастомний прохід транспілера, що успадковується від Python-класу qiskit.transpiler.TransformationPass, але використовує функцію з C-модуля розширення для всієї своєї бізнес-логіки. Це виглядає так:

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

Точні деталі цього проходу не важливі для цього посібника. Якщо тебе цікавить, можеш переглянути документацію API AddSpectatorMeasures в qiskit-addon-utils. Цей посібник створює простий порт цього проходу без підтримки операцій керування потоком.

Написання C-модуля розширення

Цей розділ присвячений власне C-розширенню. Це найскладніший файл у проекті, тому ми розіб'ємо його на етапи.

Налаштування заголовних файлів

При збірці Python-модуля розширення потрібно включити Python.h перед будь-яким іншим файлом. Щоб використовувати Qiskit C API в модулі розширення, необхідно визначити макрос QISKIT_PYTHON_EXTENSION перед включенням qiskit.h.

Наші включення тоді матимуть вигляд:

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>

Написання чистого коду C API

Далі написуй всю бізнес-логіку як чистий код Qiskit C API. Ми надамо доступ до цієї логіки в Python-просторі в наступному розділі.

Цей розділ містить лише чистий код Qiskit C API. Він використовує типи C API:

  • QkDag *, що відповідає Python-класу DAGCircuit.
  • QkTarget *, що відповідає Python-класу Target.
  • QkNeighbors — нативний тип C API, що представляє обмеження зв'язності двох кубітів.
  • QkCircuitInstruction — нативний тип C API для запиту окремих інструкцій.

Перші два є частиною нашої взаємодії з Python-простором, але при роботі з ними нам потрібно розглядати лише чистий C API. У цьому коді немає взаємодії з інтерпретатором Python.

Зверни увагу, що всі функції та символи, визначені в цьому розділі, оголошені з рівнем видимості static. Це тому, що інтерпретатор Python не буде лінкуватися проти цього модуля розширення; ми надамо інтерпретатору деталі доступних функцій у наступному розділі.

Ми не будемо зупинятися на алгоритмічних деталях цього коду; корисно використовувати змістовний прохід транспілера для демонстрації, але точна реалізація алгоритму не важлива для цього посібника.

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

Написання коду взаємодії з Python

Вся бізнес-логіка тепер визначена в чистому C. Далі її потрібно безпечно надати Python.

Для початку визнач єдину функцію, яка буде надана Python. Вона повинна дотримуватися визначеного підпису, що є виключно в термінах типів Python і виглядає як метод fn(self, *args, **kwargs). Ми маємо повертати PyObject * — це узагальнена форма будь-якого Python-об'єкта.

Повна функція виглядає так:

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

Коротко: функція:

  1. Дотримується визначеного підпису для прийому довільних Python-аргументів.
  2. Визначає простір для зберігання нативних C-об'єктів, витягнутих з Python-аргументів.
  3. Викликає функцію парсингу для вилучення нативних C-об'єктів, налаштовану зі списком очікуваних аргументів, іменованих аргументів і функцій для їх перетворення. При невдачі функція передає помилку далі.
  4. Делегує нативній C-бізнес-логіці попереднього розділу, яка мутує DAG на місці.
  5. Повертає Python-об'єкт None.

Найскладніша логіка вся всередині PyArg_ParseTupleAndKeywords. Це детально задокументовано в документації CPython щодо парсингу аргументів, до якої варто звертатися для отримання додаткової інформації.

Qiskit C API надає кілька функцій з назвами виду qk_*_convert_from_python, які призначені як функції «конвертера» для використання з функціями PyArg_Parse*. Вони відповідають ключам O& у рядку формату; тут ми використали qk_dag_convert_from_python і qk_target_convert_from_python. Ці функції позичають нативний C-об'єкт з Python-аргументу, з якого вони отримані. Це означає, що мутації поширяться в Python-простір, але також що слід бути обережним і не звільняти посилання на Python-об'єкт, що лежить в основі, поки використовується результат. Це стандартно для програмування Python C API.

Далі визначаємо інформацію про цей модуль і функцію, яку він містить, щоб передати її 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,
};

Ця таблиця методів і структура визначення модуля детальніше описані в документації CPython щодо ініціалізації модуля.

Нарешті, повідомляємо Python, як ініціалізувати модуль. Це єдина функція у C-файлі, що є публічною. Її назва повинна точно відповідати шаблону PyInit_<mod>, де <mod> — це (некваліфікована) назва модуля. У цьому випадку повна назва модуля — spectator_measures._core, а некваліфікована — _core, тому наша функція повинна називатися PyInit__core, з подвійним підкресленням.

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

PyMODINIT_FUNC і PyModuleDef_Init — обидва стандартні символи Python C API. Специфічний для Qiskit компонент — qk_import(). Критично важливо викликати цю функцію під час функції ініціалізації твого модуля; жодна функція Qiskit C API не буде доступна, поки це не буде успішно виконано.

Використання пакету з Python

Це тепер повноцінний пакет, що включає C-модуль розширення. Оскільки використовувався лише стандартний інструментарій і під час збірки не лінкуються нестандартні системні бібліотеки, процес збірки простий.

Можна використовувати будь-який сумісний з PEP-517 інструмент збірки. Як мінімальний приклад, можна виконати таку команду в кореневій директорії репозиторію для встановлення пакету.

pip install .

Це скомпілює C-модуль розширення і встановить повний Python-пакет у твоє середовище.

Приклад використання цього кастомного проходу транспілера:

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

Результат:

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