Lewati ke konten utama

Memperluas Qiskit di Python dengan C

Qiskit C API bisa digunakan di dalam modul ekstensi Python. Kamu bisa menulis bagian-bagian yang kritis terhadap performa dari ekstensi Qiskit kamu dalam C untuk mempercepatnya, lalu mendistribusikannya dengan aman ke pengguna kamu.

Panduan ini memandu kamu melalui proses mendefinisikan modul ekstensi yang lengkap, mengonfigurasi proses build-nya, dan mengeksposnya ke pengguna Python. Paket ini menyediakan port sederhana dari AddSpectatorMeasures dari Qiskit addons ke C. Ini adalah pass kustom nyata dengan kasus penggunaan nyata di Qiskit addons.

tips

Kamu mungkin merasa sumber daya eksternal berikut bermanfaat:

Qiskit C API diekspos untuk modul ekstensi Python dengan cara yang sangat mirip dengan NumPy C API. Jika kamu sebelumnya pernah memprogram ekstensi NumPy, kamu akan merasa proses Qiskit ini familier.

peringatan

Qiskit C API masih bersifat eksperimental. Jadi, belum ada antarmuka pemrograman atau biner yang sepenuhnya stabil, dan mungkin ada perubahan yang merusak di antara versi minor.

Sebagai contoh, modul ekstensi yang menggunakan Qiskit v2.4.0 saat build dijamin bekerja dengan Qiskit v2.4.1 saat runtime, tetapi mungkin rusak saat menggunakan Qiskit v2.5.0 saat runtime.

Persyaratan​

Mulai dari direktori yang bersih.

Kamu harus memiliki toolchain compiler C standar yang tersedia untuk platform kamu. Kamu juga harus memiliki versi Python yang menyertakan header C API-nya (ini standar).

Kamu sebaiknya familier dengan, atau siap untuk mencari, masing-masing fungsi dan objek yang tersedia di Qiskit C API. Kamu sebaiknya memiliki sedikit kemampuan dalam pemrograman C.

Membuat struktur direktori​

Kita akan menggunakan struktur direktori berbasis src dan sistem build sederhana berbasis setuptools. Instruksi ini seharusnya mudah diadaptasi ke sistem build apa pun yang bisa membangun modul ekstensi.

Struktur akhirnya akan terlihat seperti:

extension-module
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ setup.py
└── src
└── spectator_measures
β”œβ”€β”€ __init__.py
└── _coremodule.c

Singkatnya:

  • pyproject.toml mendefinisikan metadata statis standar tentang paket Python yang kita buat, termasuk nama, penulis, serta dependensi build- dan run-time-nya.
  • setup.py berisi konfigurasi dinamis minimal yang kita butuhkan untuk membangun modul ekstensi kita.
  • src/spectator_measures/__init__.py mendefinisikan antarmuka yang dihadapkan ke pengguna dan menyediakan kode untuk berinteraksi dengan komponen ruang-Python milik Qiskit.
  • src/spectator_measures/_coremodule.c mendefinisikan modul ekstensi C, yang akan berisi semua kode yang kritis terhadap performa dari paket kita.

Kita akan memeriksa setiap file secara detail, membangun paket beserta modul ekstensinya.

Mendefinisikan metadata paket​

Mulailah dengan mendefinisikan file pyproject.toml. Ini standar untuk proyek berbasis setuptools, meskipun qiskit merupakan persyaratan tambahan di dalam array build-system.requires, selain 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"}

Pada Qiskit v2.4, C API belum stabil di luar versi minor (sebagai contoh, C API untuk v2.4.0 akan kompatibel dengan v2.4.1 tetapi tidak dengan v2.5.0). Ke depannya, kita berniat memperluas stabilitas ini hingga ke versi mayor. Untuk saat ini, atur versi runtime Qiskit di project.dependencies agar cocok dengan versi minor yang digunakan saat build.

Di banyak proyek berbasis setuptools yang murni Python, cukup memiliki file pyproject.toml saja. Namun, modul kita membutuhkan akses ke file header Qiskit C API selama proses build-nya. Mulai dari v2.4, file-file ini disertakan dalam distribusi Python Qiskit SDK. Untuk menemukan direktori yang berisinya, jalankan qiskit.capi.get_include(). Ini menghasilkan file setup.py yang terlihat seperti:

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

Sebagian besar informasi paket didefinisikan di pyproject.toml, dan setuptools.setup() juga akan membaca file tersebut.

tips

Lihat Panduan Pengguna setuptools untuk informasi lebih lanjut tentang mengonfigurasi proyek berbasis setuptools.

Menulis wrapper ruang-Python​

Secara teknis, dimungkinkan untuk mendefinisikan semua hal dalam ekstensi Python dari C. Dalam praktiknya, lebih mudah berinteraksi dengan kode ruang-Python lainnya dari Python itu sendiri.

Paket ini mendefinisikan pass Transpiler kustom yang diturunkan dari kelas ruang-Python qiskit.transpiler.TransformationPass, tetapi menggunakan fungsi dari modul ekstensi C untuk semua logika bisnisnya. Ini terlihat seperti:

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

Detail pasti dari pass ini tidak penting untuk panduan ini. Jika kamu tertarik, kamu bisa melihat dokumentasi API AddSpectatorMeasures di qiskit-addon-utils. Panduan ini menghasilkan port sederhana dari pass tersebut, tanpa dukungan untuk operasi control-flow.

Menulis modul ekstensi C​

Bagian ini berkaitan dengan ekstensi C yang sebenarnya. Ini adalah file paling kompleks dalam proyek, jadi kita akan membaginya menjadi beberapa tahap.

Mengonfigurasi file header​

Saat membangun modul ekstensi Python, kamu harus menyertakan Python.h sebelum file lainnya. Untuk menggunakan Qiskit C API dalam modul ekstensi, kamu harus mendefinisikan makro QISKIT_PYTHON_EXTENSION sebelum menyertakan qiskit.h.

Includes kita kemudian terlihat seperti:

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>

Menulis kode C API murni​

Selanjutnya, tulis semua logika bisnis sebagai kode Qiskit C API murni. Kita akan mengekspos logika ini ke ruang-Python di bagian berikut.

Bagian ini hanya berisi kode Qiskit C API murni. Ia menggunakan tipe-tipe C API:

  • QkDag *, yang berkorespondensi dengan DAGCircuit di ruang-Python.
  • QkTarget *, yang berkorespondensi dengan Target di ruang-Python.
  • QkNeighbors, tipe C API native yang merepresentasikan batasan coupling dua-Qubit.
  • QkCircuitInstruction, tipe C API native untuk mengkueri masing-masing instruksi.

Dua yang pertama merupakan bagian dari interaksi kita dengan ruang-Python, tetapi saat bekerja dengannya, kita hanya perlu mempertimbangkan C API murni. Tidak ada interaksi dengan interpreter Python dalam kode ini.

Perhatikan bahwa semua fungsi dan simbol yang didefinisikan di bagian ini dideklarasikan dengan linkage static. Ini karena interpreter Python tidak akan melakukan link terhadap modul ekstensi ini; kita akan menyediakan detail fungsi-fungsi yang tersedia kepada interpreter di bagian berikutnya.

Kita tidak akan berkutat pada detail algoritmis dari kode ini; berguna menggunakan pass Transpiler yang bermakna untuk demonstrasi, tetapi implementasi persis dari algoritmanya tidak penting untuk panduan ini.

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

Menulis kode interaksi Python​

Semua logika bisnis kini didefinisikan dalam C murni. Selanjutnya, ia perlu diekspos dengan aman ke Python.

Untuk memulai, definisikan satu-satunya fungsi yang akan diekspos ke Python. Ini harus mengikuti signature tertentu, yang murni dalam bentuk tipe-tipe Python yang terlihat seperti metode fn(self, *args, **kwargs). Kita harus mengembalikan sebuah PyObject *, yang merupakan bentuk generik dari objek Python apa pun.

Fungsi lengkapnya terlihat seperti:

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

Singkatnya, fungsi ini:

  1. Mengikuti signature tertentu untuk menerima argumen Python sembarang.
  2. Mendefinisikan ruang untuk menyimpan objek C-native yang di-parse dari argumen Python.
  3. Memanggil fungsi parsing untuk mengekstrak objek C-native, dikonfigurasi dengan daftar argumen yang diharapkan, keyword argument, dan fungsi yang digunakan untuk mengonversinya. Jika ini gagal, fungsi tersebut mempropagasi error.
  4. Mendelegasikan ke logika bisnis C-native dari bagian sebelumnya, yang memutasi DAG di tempat.
  5. Mengembalikan objek None di ruang-Python.

Logika paling kompleks ada di dalam PyArg_ParseTupleAndKeywords. Ini terdokumentasi dengan baik di dokumentasi CPython tentang parsing argumen, yang sebaiknya kamu lihat untuk informasi lebih lanjut.

Qiskit C API menyediakan beberapa fungsi dengan nama seperti qk_*_convert_from_python, yang dirancang sebagai fungsi "konverter" untuk digunakan dengan fungsi PyArg_Parse*. Fungsi-fungsi ini berkorespondensi dengan kunci O& di string format; di sini, kita menggunakan qk_dag_convert_from_python dan qk_target_convert_from_python. Fungsi-fungsi ini meminjam objek C-native dari argumen Python asalnya. Ini berarti mutasi akan terpropagasi ke ruang-Python, tetapi juga berarti kamu harus berhati-hati untuk tidak melepaskan referensi kamu ke objek Python yang menjadi penopangnya, saat menggunakan hasilnya. Ini standar untuk pemrograman Python C API.

Selanjutnya, kita definisikan informasi tentang modul ini dan fungsi yang ada di dalamnya, sehingga kita bisa meneruskannya ke ruang-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,
};

Tabel metode dan struktur definisi-modul ini dijelaskan lebih detail di dokumentasi CPython tentang inisialisasi modul.

Terakhir, beri tahu Python cara menginisialisasi modul. Ini adalah satu-satunya fungsi di file C yang diekspor. Namanya harus sama persis dengan pola PyInit_<mod>, di mana <mod> adalah nama modul (tanpa kualifikasi). Dalam kasus ini, nama modul yang terkualifikasi penuh adalah spectator_measures._core, dan nama tanpa kualifikasinya adalah _core, jadi fungsi kita harus dinamai PyInit__core, dengan garis bawah ganda.

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

Simbol PyMODINIT_FUNC dan PyModuleDef_Init keduanya merupakan pemrograman Python C API standar. Komponen yang spesifik untuk Qiskit adalah qk_import(). Sangat penting kamu memanggil fungsi ini selama fungsi inisialisasi modul kamu; kamu tidak akan bisa memanggil fungsi Qiskit C API apa pun sampai ini berhasil dijalankan.

Menggunakan paket dari Python​

Ini sekarang menjadi paket yang lengkap, termasuk modul ekstensi C. Karena hanya tooling standar yang digunakan, dan tidak ada library sistem non-standar yang di-link saat build time, proses build-nya sederhana.

Kamu bisa menggunakan alat build apa pun yang kompatibel dengan PEP-517. Sebagai contoh minimal, kamu bisa menjalankan perintah berikut di root repositori untuk menginstal paket.

pip install .

Ini mengkompilasi modul ekstensi C dan menginstal paket Python lengkap di environment kamu.

Contoh penggunaan pass Transpiler kustom ini adalah:

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

Hasilnya adalah:

β”Œβ”€β”€β”€β” β–‘
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