Python、C言語でQiskitを拡張する
Qiskit C APIは、 Python 拡張モジュール内で使用できます。 Qiskit拡張機能のうち、パフォーマンスが重要な部分はC言語で記述して高速化することができ、 その後、これらをユーザーに安全に配布することができます。
このガイドでは、拡張モジュールの定義、ビルドプロセスの設定、およびPythonユーザーへの公開の手順について解説します このパッケージは
[、QiskitのアドオンAddSpectatorMeasures] をCAddSpectatorMeasures-code 言語へ移植したシンプルなものです。 これは、Qiskitアドオンにおいて実際のユースケースを持つ、本格的なカスタム
パスです。
以下の外部リソースが参考になるかもしれません:
Qiskit C APIは、NumPyのC APIと非常によく似た形で、Python拡張モジュールに対して公開されています 以前に NumPy 拡張機能を開発したことがある方なら、Qiskitのプロセスは 馴染み深いものとなるでしょう。
Qiskit C API はまだ実験段階です。 したがって、現時点では完全に安定したプログラミングインターフェースや バイナリインターフェースは存在せず、マイナーバージョン間で互換性を損なう変更が行われる可能性があります。
たとえば、ビルド時に Qiskit v2.4.0 を使用している拡張モジュールは、実行時に Qiskit v2.4.1 を使用する場合は確実に動作しますが、実行時に Qiskit v2.5.0 を使用すると動作しなくなる可能性があります
要件
「インストール 」のトピックに記載されている手順に従って、以下のものをインストールしてください:
- お使いのプラットフォーム向けの標準的なCコンパイラ・ツールチェーン
- C APIヘッダーを含む Python のバージョン。
また、 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ユーザー向けのインターフェースを定義し、QiskitのPython-spaceコンポーネントと連携するためのコードを提供しますsrc/spectator_measures/_coremodule.cC拡張モジュールを定義します。このモジュールには、当パッケージの パフォーマンスに重要なコードがすべて含まれます。
各ファイルを詳細に検証し、拡張モジュールを組み込んでパッケージを構築していきます。
パッケージのメタデータを定義する
まず、ファイル pyproject.toml の定義から始めます。 これは -ベース setuptoolsのプロジェクトでは標準的な仕様ですが、
配列 build-system.requires には に加えて、
という追加 setuptools要件がありますqiskit。
[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のランタイムバージョンを、
project.dependencies ビルド時に使用されたマイナーバージョンと一致するように設定します。
多くの純粋な Pythonsetuptoolsベースのプロジェクトでは、その
pyproject.toml ファイルさえあれば十分です。 ただし、当モジュールはビルドプロセス中に
Qiskit C APIのヘッダーファイルにアクセスする必要があります。 v2.4 以降、これらは Qiskit SDK Python のディストリビューションに含まれています。
それらが入っているディレクトリを見つけるには、以下を実行してください qiskit.capi.get_include()。
その結果、次のようなファイル 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-spaceクラスを継承するカスタムトランスパイラ・パスを定義していますが、そのビジネスロジックはすべてC拡張モジュールの関数を使用しています。 これは次のようなものです:
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このパスに関する詳細な内容は、このガイドにおいては重要ではありません。 ご興味があれば、
以下のサイトをご覧ください AddSpectatorMeasures APIドキュメント
qiskit-addon-utils. このガイドでは、そのパスの簡易な移植版を作成します。
ただし、制御フロー操作はサポートしていません。
C拡張モジュールを作成する
以下のリソースが参考になるかもしれません:
このセクションでは、実際のC言語拡張について扱います。 これはこのプロジェクトの中で最も複雑なファイルですので、 段階に分けて進めていきます。
ヘッダーファイルの設定
Python の拡張モジュールを作成する際は、他のどのファイルよりも先に を Python.h インクルードする必要があります。
拡張モジュールでQiskit C APIを使用するには、インクルードする前に
QISKIT_PYTHON_EXTENSION マクロを定義 qiskit.hする必要があります。
その結果、当社のコードは次のようになります:
#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 *、 PythonDAGCircuit-空間に対応する。QkTarget *、 PythonTarget-空間に対応する。QkNeighbors、2量子ビットの結合制約を表すネイティブC API型。QkCircuitInstruction、個々の命令を照会するためのネイティブC API型。
最初の2つは Python 空間とのやり取りの一部ですが、これらを扱う際には、 純粋なC APIのみを考慮すれば十分です。 このコードでは、 Python インタプリタとのやり取りは一切行われません。
なお、このセクションで定義されるすべての関数およびシンボルは、リンク static スコープで宣言されています。
これは、Pythonのインタプリタがこの拡張モジュールに対してリンクを行わないためです。利用可能な関数の詳細については、次のセクションで説明します
このコードのアルゴリズムの詳細については深く立ち入らない。実演のために意味のある トランスパイラ・パスを使用することは参考になるが、アルゴリズムの正確な実装は このガイドにとって重要ではない。
/**
* 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 に対して公開する唯一の関数を定義します。 これは、
fn(self, *args, **kwargs)メソッドのような形式のPython型のみで構成される、定義済みのシグネチャに従う必要があります。 Pythonの任意のオブジェクトを表すジェネリック型である``PyObject *aを返さなければなりません
関数全体は次のようになります:
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;
}簡単に言えば、この関数は:
- 定義されたシグネチャに従い、任意の Python 引数を受け入れます。
- Python の引数から解析されたCネイティブオブジェクトを格納するための領域を定義します。
- Cネイティブオブジェクトを抽出するための解析関数を呼び出します。この関数は、期待される引数、キーワード引数、およびそれらを変換するために使用する関数のリストで構成されます これが失敗した場合、 関数はエラーを伝播します。
- 前のセクションで説明したCネイティブのビジネスロジックへのデリゲートであり、これによってDAGがその場で変更されます。
- Python
None-spaceオブジェクトを返します。
最も複雑なロジックはすべて内部に組み込まれています PyArg_ParseTupleAndKeywords。 これについては、
に詳しく記載されていますので引数の解析に関する
CPythonのドキュメント、
詳細についてはそちらをご参照ください。
Qiskit C API には、 qk_*_convert_from_pythonといった名前の関数がいくつか用意されており、これらは
関数 PyArg_Parse*と組み合わせて使用する「コンバータ」関数として設計されています。 これらはフォーマット文字列
O& 内のキーに対応しています。ここでは、と
を使用qk_dag_convert_from_pythonしましたqk_target_convert_from_python。 これらの関数は、その元となったPython
引数からCネイティブオブジェクトを借用します
。 これは、変異が Python 空間に伝播することを意味しますが、同時に、
結果を使用する際には、それらを支える Python オブジェクトへの参照を解放しないよう
注意する必要があることを意味します。 これは、 Python のC APIプログラミングにおける標準的な手法です。
次に、このモジュールとそこに含まれる関数に関する情報を定義し、 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,
};このメソッドテーブルとモジュール定義の構造については、CPythonの モジュール初期化に関するドキュメントで より詳しく説明されています。
最後に、 Python にモジュールの初期化方法を指定してください。 これは、このC
ファイルでエクスポートされる唯一の関数です。 その名前はパターンと完全に一致する必要があります
PyInit_<mod>。ここで <mod> は(修飾子なしの)モジュール名です。 この場合、完全修飾モジュール名は
でありspectator_measures._core、非修飾名は
であるため_core、関数は
(二重アンダースコア付き)PyInit__coreで呼び出す必要があります。
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-compatible のビルドツールであれば、どれでも使用できます。 ごく簡単な例として、リポジトリのルートディレクトリで次のコマンドを実行すると、パッケージをインストールできます。
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