メインコンテンツへスキップ

CでQiskitをPython向けに拡張する

Qiskit C APIはPython拡張モジュール内で使用できます。 Qiskit拡張モジュールのパフォーマンスが重要なセクションをCで記述して高速化し、 ユーザーに安全に配布することができます。

このガイドでは、完全な拡張モジュールの定義、ビルドプロセスの設定、Pythonユーザーへの公開というプロセスを順を追って説明します。このパッケージはQiskit addonsのAddSpectatorMeasuresのCへの簡易ポートを提供します。これはQiskit addonsで実際のユースケースを持つ、実用的なカスタムパスです。

ヒント

以下の外部リソースが参考になるかもしれません:

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のパイソンスペースのコンポーネントとのインターフェースのためのコードを提供します。
  • src/spectator_measures/_coremodule.cはC拡張モジュールを定義し、パッケージのパフォーマンスが重要なコードをすべて含みます。

各ファイルを詳しく検討しながら、拡張モジュールを含むパッケージを構築していきます。

パッケージメタデータの定義

まずpyproject.tomlファイルを定義します。これはsetuptoolsベースのプロジェクトの標準的な形式ですが、build-system.requires配列にsetuptoolsに加えてqiskitが追加の要件として含まれています。

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はマイナーバージョン外ではまだ安定していません(たとえば、v2.4.0のC APIはv2.4.1と互換性がありますが、v2.5.0とは互換性がありません)。将来的にはメジャーバージョン間での安定性を拡張する予定です。現時点では、project.dependenciesのQiskitのランタイムバージョンをビルド時に使用するマイナーバージョンに一致させてください。

多くの純粋なPythonのsetuptoolsベースプロジェクトでは、pyproject.tomlファイルだけで十分です。しかし、このモジュールのビルドプロセスではQiskit C APIヘッダーファイルへのアクセスが必要です。v2.4以降、これらはQiskit SDK Pythonディストリビューションに含まれています。 それらを含むディレクトリを特定するには、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

このパスの詳細な内容はこのガイドでは重要ではありません。興味がある場合は、qiskit-addon-utilsAddSpectatorMeasures APIドキュメントを参照してください。このガイドでは制御フロー操作のサポートなしで、そのパスの簡易ポートを作成します。

C拡張モジュールの記述

このセクションでは実際のC拡張モジュールについて説明します。これはプロジェクト内で最も複雑なファイルなので、段階に分けて説明します。

ヘッダーファイルの設定

Python拡張モジュールをビルドする場合、他のファイルより前にPython.hをインクルードする必要があります。 拡張モジュールでQiskit C APIを使用するには、qiskit.hをインクルードする前にマクロQISKIT_PYTHON_EXTENSIONを定義する必要があります。

インクルードは次のようになります:

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:2Qubitカップリング制約を表すネイティブC API型です。
  • QkCircuitInstruction:個々の命令をクエリするためのネイティブC API型です。

最初の2つは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に公開する唯一の関数を定義します。これはfn(self, *args, **kwargs)メソッドのようなPython型のみを使う定義されたシグネチャに従う必要があります。任意のPythonオブジェクトの汎用的な形であるPyObject *を返す必要があります。

完全な関数は次のようになります:

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. Python引数から解析されたCネイティブオブジェクトを格納するスペースを定義します。
  3. 期待される引数のリスト、キーワード引数、および変換に使用する関数を設定した解析関数を呼び出してCネイティブオブジェクトを抽出します。失敗した場合、関数はエラーを伝播します。
  4. DAGをインプレースで変更する前のセクションのCネイティブのビジネスロジックに処理を委譲します。
  5. PythonスペースのNoneオブジェクトを返します。

最も複雑なロジックはすべてPyArg_ParseTupleAndKeywords内にあります。これは引数の解析に関するCPythonドキュメントに詳しく説明されており、詳細はそちらを参照してください。

Qiskit C APIはqk_*_convert_from_pythonという名前のいくつかの関数を提供しており、これらはPyArg_Parse*関数と組み合わせて使う「コンバーター」関数として設計されています。これらはフォーマット文字列のO&キーに対応しており、ここではqk_dag_convert_from_pythonqk_target_convert_from_pythonを使用しました。これらの関数は、派生元のPython引数からCネイティブオブジェクトを 借用 します。つまり、変更は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_FUNCPyModuleDef_Initシンボルはどちらも標準的なPython C APIプログラミングです。Qiskit固有のコンポーネントはqk_import()です。モジュールの初期化関数中にこの関数を呼び出すことが重要です。これが正常に実行されるまで、Qiskit C API関数を呼び出すことはできません。

Pythonからパッケージを使う

これで拡張モジュールを含む完全なパッケージが完成しました。標準的なツールのみが使用されており、ビルド時に非標準のシステムライブラリがリンクされないため、ビルドプロセスはシンプルです。

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