Skip to main content
IBM Quantum Platform

Python 에서 C로 키스킷 확장하기

Qiskit C API는 Python 확장 모듈 내에서 사용할 수 있습니다. Qiskit 확장 기능 중 성능이 중요한 부분은 C 언어로 작성하여 처리 속도를 높일 수 있으며, 이후 이를 사용자에게 안전하게 배포할 수 있습니다.

이 가이드에서는 완전한 확장 모듈을 정의하고, 빌드 프로세스를 구성하며, 이를 Python 사용자에게 제공하는 과정을 단계별로 안내합니다 이 패키지는 [Qiskit AddSpectatorMeasures 애드온을] CAddSpectatorMeasures-code 언어로 간단히 포팅한 것입니다. 이것은 Qiskit 애드온에서 실제 사용 사례가 있는 진정한 커스텀 패스입니다.

Tip

다음 외부 자료들이 도움이 될 수 있습니다:

Python 용 확장 모듈에 대해 Qiskit C API는 NumPy C API와 매우 유사한 방식으로 제공됩니다. 이전에 NumPy 확장 기능을 프로그래밍해 본 적이 있다면, Qiskit의 작업 과정이 익숙하게 느껴질 것입니다.

Warning

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.c 이 코드는 C 확장 모듈을 정의하며, 이 모듈에는 패키지의 모든 성능에 중요한 코드가 포함될 것입니다.

각 파일을 자세히 살펴보고, 확장 모듈을 포함하여 패키지를 구성해 보겠습니다.


패키지 메타데이터 정의

먼저 파일을 pyproject.toml 정의하세요. 이는 -기반 setuptools프로젝트의 표준이지만, build-system.requires배열에서는 qiskit에 더해 가 추가적인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"}

빌드 시 사용된 마이너 버전과 일치하도록 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() 파일을 읽어들입니다.

Tip
  • 기반 프로젝트setuptools 구성에 대한 자세한 내용은 사용자 setuptools가이드를 참조하십시오.

Python -space 래퍼 작성하기

기술적으로 C 언어를 사용하여 Python 확장 기능의 모든 요소를 정의하는 것이 가능합니다. 실제로는 Python 내에서 다른 Python 공간의 코드와 연동하는 것이 더 쉽습니다.

이 패키지는 Python-space 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

이 가이드에서는 이 패스의 구체적인 내용은 중요하지 않습니다. 관심이 있으시다면 다음 내용을 참고해 보세요 AddSpectatorMeasures API 문서 qiskit-addon-utils. 이 가이드는 해당 패스의 간단한 포트를 생성하며, 제어 흐름 연산에 대한 지원은 포함하지 않습니다.


C 확장 모듈 작성하기

이 섹션에서는 실제 C 확장 기능에 대해 다룹니다. 이 파일은 프로젝트에서 가장 복잡한 파일이므로, 단계별로 나누어 진행할 것입니다.

헤더 파일 설정하기

Python 확장 모듈을 빌드할 때는 다른 어떤 파일보다 먼저 Python.h 를 포함해야 합니다. 확장 모듈에서 Qiskit C API를 사용하려면, 해당 매크로를 포함하기 QISKIT_PYTHON_EXTENSION 전에 먼저 qiskit.h정의해야 합니다.

그러면 우리의 include 문은 다음과 같이 됩니다:

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, 2큐비트 결합 제약 조건을 나타내는 네이티브 C API 유형입니다.
  • QkCircuitInstruction, 개별 명령어를 조회하기 위한 네이티브 C API 유형입니다.

처음 두 가지는 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 타입들로만 구성되어야 합니다. 우리는 anyPython 객체의 제네릭 형식인 ``PyObject *a를 반환해야 합니다

전체 함수는 다음과 같습니다:

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. 이전 섹션에서 설명한 C 네이티브 비즈니스 로직에 대한 델리게이트로, DAG를 그 자리에서 수정합니다.
  5. Python -space None 객체를 반환합니다.

가장 복잡한 논리는 모두 그 안에 담겨 있습니다 PyArg_ParseTupleAndKeywords. 이에 대한 내용은 에 자세히 설명되어 인수 구문 분석에 관한 CPython 문서 있으므로, 자세한 내용은 해당 문서를 참고하시기 바랍니다.

Qiskit C API는 와 qk_*_convert_from_python같은 이름의 여러 함수를 제공하며, 이 함수들은 함수와 PyArg_Parse* 함께 사용하기 위한 “변환기” 함수로 설계되었습니다. 이는 포맷 문자열의 키에 O& 해당합니다. 여기서는 와 qk_target_convert_from_pythonqk_dag_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 '|' 기호는 모두 C API 프로그래밍의 표준 Python 입니다. 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
이 페이지가 도움이 되었습니까?
GitHub에서 버그, 오타를 보고하거나 컨텐츠를 요청하십시오.