انتقل إلى المحتوى الرئيسي

توسعة 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 لا تزال تجريبية. وبالتالي، لا توجد بعد واجهة برمجية أو ثنائية مستقرة بالكامل، وقد تكون هناك تغييرات جذرية بين الإصدارات الفرعية.

على سبيل المثال، وحدة امتداد تستخدم 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-space الخاصة بـ 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 ليتطابق مع الإصدار الفرعي المستخدم في وقت البناء.

في كثير من مشاريع setuptools النقية المكتوبة بـ Python، يكفي وجود ملف 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-space

من الناحية التقنية يمكن تعريف كل شيء في امتداد Python من C. أما في الممارسة العملية، فمن الأسهل التفاعل مع كود Python-space من Python نفسه.

تُعرّف هذه الحزمة تمريرة ترانسبايلر مخصصة تشتق من فئة qiskit.transpiler.TransformationPass في Python-space، لكنها تستخدم دالة من وحدة امتداد 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-space في القسم التالي.

يحتوي هذا القسم على كود Qiskit C API النقي فقط. يستخدم أنواع C API:

  • QkDag *، يقابل DAGCircuit في Python-space.
  • QkTarget *، يقابل Target في Python-space.
  • QkNeighbors، نوع C API أصلي يمثل قيود الإقران ثنائية الكيوبت.
  • QkCircuitInstruction، نوع C API أصلي للاستعلام عن التعليمات الفردية.

يشكّل الأولان جزءاً من تفاعلنا مع Python-space، لكن عند التعامل معهما، نحتاج فقط إلى النظر في 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-space 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-space، لكن أيضاً يجب الانتباه إلى عدم التخلي عن مرجعك لكائن Python الذي يُشكّل قاعدتهم أثناء استخدام النتيجة. هذا أمر معياري في برمجة Python C API.

بعد ذلك، نُعرّف المعلومات حول هذه الوحدة والدالة التي تحتويها، حتى نتمكن من تمريرها إلى 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,
};

جدول الطرق وهيكل تعريف الوحدة هذان موصوفان بمزيد من التفصيل في توثيق 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