Rozšíření Qiskitu v Pythonu pomocí C
Qiskit C API lze použít v rámci rozšiřujících modulů Pythonu. Výkonnostně kritické části svých Qiskit rozšíření můžeš napsat v C a urychlit je, přičemž je pak bezpečně distribuuješ svým uživatelům.
Tento průvodce tě provede procesem definice kompletního rozšiřujícího modulu, nastavením
jeho sestavovacího procesu a zpřístupněním pro uživatele Pythonu. Balíček poskytuje jednoduchý port
AddSpectatorMeasures z Qiskit addons do C. Jde o skutečný vlastní
průchod se skutečným případem použití v Qiskit addons.
Může se ti hodit následující externí zdroje:
Qiskit C API je pro rozšiřující moduly Pythonu zpřístupněno velmi podobným způsobem jako NumPy C API. Pokud jsi již dříve programoval/a rozšíření pro NumPy, bude ti proces s Qiskitem povědomý.
Qiskit C API je stále experimentální. Proto zatím neexistuje plně stabilní programovací nebo binární rozhraní a mezi vedlejšími verzemi mohou nastat zásadní změny.
Například rozšiřující modul používající Qiskit v2.4.0 při sestavení bude zaručeně fungovat s Qiskitem v2.4.1 za běhu, ale může přestat fungovat při použití Qiskitu v2.5.0 za běhu.
Požadavky
Začni v čistém adresáři.
Musíš mít k dispozici standardní sadu nástrojů kompilátoru C pro svou platformu. Dále musíš mít verzi Pythonu, která zahrnuje hlavičkové soubory jeho C API (toto je standardní).
Měl/a by ses orientovat ve funkcích a objektech dostupných v Qiskit C API, případně být připraven/a je vyhledávat. Měl/a by ses také trochu orientovat v programování v C.
Vytvoření adresářové struktury
Použijeme adresářovou strukturu založenou na src a jednoduchý sestavovací systém postavený na
setuptools. Tyto instrukce by mělo být snadné přizpůsobit jakémukoli sestavovacímu systému, který
umí sestavovat rozšiřující moduly.
Výsledná struktura bude vypadat takto:
extension-module
├── pyproject.toml
├── setup.py
└── src
└── spectator_measures
├── __init__.py
└── _coremodule.c
Stručně řečeno:
pyproject.tomldefinuje standardní statická metadata o vytvářeném Python balíčku, včetně jeho názvu, autora a závislostí při sestavení a za běhu.setup.pyobsahuje minimální dynamickou konfiguraci potřebnou k sestavení rozšiřujícího modulu.src/spectator_measures/__init__.pydefinuje uživatelské rozhraní a poskytuje kód pro spolupráci s komponenty Qiskitu v Python prostoru.src/spectator_measures/_coremodule.cdefinuje rozšiřující modul v C, který bude obsahovat veškerý výkonnostně kritický kód balíčku.
Každý soubor si podrobně probereme a postupně sestavíme balíček s jeho rozšiřujícím modulem.
Definice metadat balíčku
Začni definicí souboru pyproject.toml. Toto je standardní postup pro projekt postavený na
setuptools, přičemž qiskit je navíc požadavek v poli build-system.requires
vedle 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"}
Od verze Qiskit v2.4 není C API mimo vedlejší verze stabilní (například C API pro v2.4.0 bude
kompatibilní s v2.4.1, ale ne s v2.5.0). V budoucnu hodláme tuto stabilitu rozšířit na hlavní
verze. Prozatím nastav verzi Qiskitu za běhu v project.dependencies tak, aby odpovídala vedlejší
verzi použité při sestavení.
V mnoha čistě Pythonovských projektech postavených na setuptools by soubor pyproject.toml
stačil. Náš modul však potřebuje přístup k hlavičkovým souborům Qiskit C API během procesu
sestavení. Počínaje verzí v2.4 jsou tyto soubory součástí Python distribucí Qiskit SDK.
Pro nalezení adresáře, který je obsahuje, spusť qiskit.capi.get_include().
Výsledný soubor setup.py vypadá takto:
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])
Většina informací o balíčku je definována v pyproject.toml a setuptools.setup() tento soubor
také načte.
Více informací o konfiguraci projektů postavených na setuptools najdeš v
Uživatelské příručce setuptools.
Napsání Python-prostorového obalu
Technicky je možné definovat vše v rozšíření pro Python přímo v C. V praxi je však snazší pracovat s jiným kódem v Python prostoru přímo z Pythonu.
Tento balíček definuje vlastní průchod transpileru, který vychází z Python-prostorové třídy
qiskit.transpiler.TransformationPass, ale pro veškerou svou obchodní logiku využívá funkci
z rozšiřujícího modulu v C. Vypadá to takto:
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
Přesné detaily tohoto průchodu nejsou pro tento průvodce podstatné. Pokud tě to zajímá, můžeš
nahlédnout do dokumentace API AddSpectatorMeasures v
qiskit-addon-utils. Tento průvodce vytváří jednoduchý port daného
průchodu bez podpory pro operace řízení toku.
Napsání rozšiřujícího modulu v C
Mohou se ti hodit následující zdroje:
Tato část se věnuje samotnému rozšíření v C. Jde o nejsložitější soubor v projektu, proto ho rozdělíme do etap.
Konfigurace hlavičkových souborů
Při sestavování rozšiřujícího modulu Pythonu musíš vložit Python.h před jakýkoli jiný soubor.
Abys mohl/a v rozšiřujícím modulu použít Qiskit C API, musíš před vložením qiskit.h definovat
makro QISKIT_PYTHON_EXTENSION.
Naše vkládání hlavičkových souborů pak vypadá takto:
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>
Napsání čistého kódu C API
Dále napiš veškerou obchodní logiku jako čistý kód Qiskit C API. Tuto logiku zpřístupníme v Python prostoru v následující části.
Tato část obsahuje pouze čistý kód Qiskit C API. Používá typy C API:
QkDag *, odpovídajícíDAGCircuitv Python prostoru.QkTarget *, odpovídajícíTargetv Python prostoru.QkNeighbors, nativní typ C API reprezentující omezení spojení dvou qubitů.QkCircuitInstruction, nativní typ C API pro dotazování na jednotlivé instrukce.
První dva tvoří součást naší interakce s Python prostorem, ale při práci s nimi stačí uvažovat pouze čisté C API. V tomto kódu nedochází k žádné interakci s interpretem Pythonu.
Všimni si, že všechny funkce a symboly definované v této části jsou deklarovány s vazbou static.
Důvodem je, že interpret Pythonu se na tento rozšiřující modul nebude linkovat; podrobnosti
o dostupných funkcích mu předáme v další části.
Na algoritmické detaily tohoto kódu se nebudeme zaměřovat; je užitečné použít smysluplný průchod transpileru pro demonstraci, ale přesná implementace algoritmu pro tento průvodce není důležitá.
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;
}
Napsání kódu pro interakci s Pythonem
Veškerá obchodní logika je nyní definována v čistém C. Dále ji potřebujeme bezpečně zpřístupnit Pythonu.
Začni definicí jediné funkce, která bude zpřístupněna Pythonu. Tato funkce musí
splňovat definovaný podpis, který je výhradně v pojmech Python typů, které vypadají jako metoda
fn(self, *args, **kwargs). Musíme vrátit PyObject *, což je obecná forma jakéhokoli Python
objektu.
Kompletní funkce vypadá takto:
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;
}
Stručně řečeno, funkce:
- Splňuje definovaný podpis pro přijímání libovolných Python argumentů.
- Definuje prostor pro uložení nativních C objektů extrahovaných z Python argumentů.
- Volá funkci pro parsování, která extrahuje nativní C objekty, nakonfigurovanou se seznamem očekávaných argumentů, klíčových argumentů a funkcí pro jejich konverzi. Pokud to selže, funkce chybu dále propaguje.
- Deleguje na nativní C obchodní logiku z předchozí části, která mění DAG na místě.
- Vrací Python-prostorový objekt
None.
Nejsložitější logika se nachází uvnitř PyArg_ParseTupleAndKeywords. Tato funkce je dobře
zdokumentována v dokumentaci CPythonu o parsování argumentů,
na kterou se doporučujeme obrátit pro další informace.
Qiskit C API poskytuje několik funkcí s názvy jako qk_*_convert_from_python, které jsou
navrženy jako „konverzní" funkce pro použití s funkcemi PyArg_Parse*. Odpovídají klíčům O&
ve formátovacím řetězci; zde jsme použili qk_dag_convert_from_python a
qk_target_convert_from_python. Tyto funkce vypůjčují nativní C objekt z Python argumentu,
ze kterého jsou odvozeny. To znamená, že mutace se propagují do Python prostoru, ale také to, že
bys měl/a dbát na to, abys neuvolnil/a svůj odkaz na Python objekt, který je podkládá, dokud
výsledek používáš. Toto je standardní praxe při programování Python C API.
Dále definujeme informace o tomto modulu a funkci, kterou obsahuje, abychom je mohli předat Python prostoru:
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,
};
Tabulka metod a struktura definice modulu jsou podrobněji popsány v dokumentaci CPythonu o inicializaci modulu.
Nakonec Pythonu sdělíme, jak modul inicializovat. Toto je jediná exportovaná funkce v souboru C.
Její název musí přesně odpovídat vzoru PyInit_<mod>, kde <mod> je (nekvalifikovaný) název
modulu. V tomto případě je plně kvalifikovaný název modulu spectator_measures._core a
nekvalifikovaný název je _core, takže naše funkce se musí jmenovat PyInit__core s dvojitým
podtržítkem.
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 a PyModuleDef_Init jsou standardní součásti Python C API programování.
Složkou specifickou pro Qiskit je qk_import(). Je nezbytně nutné tuto funkci volat během
inicializační funkce modulu; dokud nebude úspěšně vykonána, nebude možné volat žádné funkce
Qiskit C API.
Použití balíčku z Pythonu
Toto je nyní kompletní balíček včetně rozšiřujícího modulu v C. Protože bylo použito pouze standardní nástroje a při sestavení se nelinkují žádné nestandardní systémové knihovny, je proces sestavení jednoduchý.
Můžeš použít libovolný sestavovací nástroj kompatibilní s PEP-517. Jako minimální příklad můžeš spustit následující příkaz v kořeni repozitáře k instalaci balíčku.
pip install .
Tím se zkompiluje rozšiřující modul v C a nainstaluje se kompletní Python balíček do tvého prostředí.
Příklad použití tohoto vlastního průchodu transpileru je:
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()
Výsledkem je:
┌───┐ ░
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