Skip to main content
IBM Quantum Platform

Amplie o Qiskit em Python com C

A API C do Qiskit pode ser utilizada nos módulos de extensão d Python. Você pode escrever as seções críticas para o desempenho das suas extensões do Qiskit em C para acelerá-las e, em seguida, distribuí-las com segurança aos seus usuários.

Este guia orienta você pelo processo de definição de um módulo de extensão completo, configuração de seu processo de compilação e disponibilização para os usuários d Python. O pacote oferece uma adaptação simples dosAddSpectatorMeasures complementos do Qiskit para C. Este é um pass personalizado com um caso de uso real nos complementos do Qiskit.

Tip

Os seguintes recursos externos podem ser úteis para você:

A API C do Qiskit é disponibilizada para os módulos de extensão do Python de maneira muito semelhante à API C do NumPy . Se você já programou uma extensão do NumPy, o processo do Qiskit lhe parecerá familiar.

Warning

A API C do Qiskit ainda está em fase experimental. Portanto, ainda não existe uma interface de programação ou binária totalmente estável, e pode haver alterações que causem incompatibilidade entre versões secundárias.

Por exemplo, um módulo de extensão que utilize o Qiskit v2.4.0 durante a compilação tem a garantia de funcionar com o Qiskit v2.4.1 em tempo de execução, mas pode deixar de funcionar se for utilizado o Qiskit v2.5.0 em tempo de execução.


Requisitos

Siga as instruções do tópico “Instalação” para instalar o seguinte:

  • A cadeia de ferramentas padrão do compilador C para sua plataforma
  • Uma versão do Python que inclui seus cabeçalhos da API em C.

Você também deve estar familiarizado com as funções e objetos disponíveis na API C do Qiskit, ou estar preparado para consultá-los, e deve ter algum conhecimento de programação em C.

Comece com um diretório vazio.


Crie a estrutura de diretórios

Usaremos uma estrutura de srcdiretórios baseada em e um sistema de compilação simples setuptoolsbaseado em. Essas instruções devem ser facilmente adaptáveis a qualquer sistema de compilação capaz de compilar módulos de extensão.

A estrutura final ficará assim:

extension-module
├── pyproject.toml
├── setup.py
└── src
    └── spectator_measures
        ├── __init__.py
        └── _coremodule.c

Em resumo:

  • pyproject.toml define os metadados estáticos padrão sobre o pacote Python que estamos criando, incluindo seu nome, autor e dependências de compilação e de tempo de execução.
  • setup.py contém a configuração dinâmica mínima necessária para compilar nosso módulo de extensão.
  • src/spectator_measures/__init__.py define a interface voltada para o usuário e fornece código para interagir com os componentes do espaço de dados Python do Qiskit.
  • src/spectator_measures/_coremodule.c define o módulo de extensão C, que conterá todo o código crítico para o desempenho do nosso pacote.

Analisaremos cada arquivo detalhadamente, montando o pacote com seu módulo de extensão.


Definir os metadados do pacote

Comece definindo o pyproject.toml arquivo. Isso é padrão para um projeto setuptoolsbaseado em , embora qiskit seja um requisito adicional na build-system.requires matriz, além de 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"}

Defina a versão de tempo de execução do Qiskit para project.dependencies que corresponda à versão secundária usada no momento da compilação.

Em muitos projetos baseados exclusivamente setuptools no Python, bastaria ter o pyproject.toml arquivo. No entanto, nosso módulo precisa acessar os arquivos de cabeçalho da API C do Qiskit durante seu processo de compilação. A partir da versão v2.4, esses recursos estão incluídos nas distribuições Qiskit SDK e Python. Para localizar o diretório que os contém, execute qiskit.capi.get_include(). Isso resulta em um setup.py arquivo com a seguinte aparência:

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

A maior parte das informações do pacote está definida em pyproject.toml, e setuptools.setup() também irá ler esse arquivo.

Tip

Consulte o setuptools Guia do Usuário para obter mais informações sobre como configurar projetos setuptoolsbaseados em.


Escreva o wrapper de espaço Python

É tecnicamente possível definir tudo em uma extensão do Python a partir do C. Na prática, é mais fácil interagir com outro código do espaço Python a partir do próprio Python.

Este pacote define uma etapa de transpilador personalizada que deriva da classe Pythonqiskit.transpiler.TransformationPass -space, mas utiliza uma função do módulo de extensão C para toda a sua lógica de negócios. Fica assim:

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

Os detalhes exatos desse passe não são relevantes para este guia. Se você estiver interessado, pode consultar o AddSpectatorMeasures Documentação da API em qiskit-addon-utils. Este guia apresenta uma adaptação simples dessa passagem, sem suporte para operações de fluxo de controle.


Escreva o módulo de extensão em C

Tip

Os seguintes recursos podem ser úteis para você:

Esta seção trata da extensão em C propriamente dita. Este é o arquivo mais complexo do projeto, por isso vamos dividi-lo em etapas.

Configure os arquivos de cabeçalho

Ao criar um módulo de extensão do Python, você deve incluir Python.h antes de qualquer outro arquivo. Para usar a API C do Qiskit em um módulo de extensão, é necessário definir a macro QISKIT_PYTHON_EXTENSION antes de incluí-la qiskit.h.

Nossas instruções ficam assim:

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>

Escreva o código da API em C puro

Em seguida, escreva toda a lógica de negócios como código puro da API C do Qiskit. Apresentaremos essa lógica em um espaço d Python es na seção a seguir.

Esta seção contém apenas código da API C do Qiskit. Ele utiliza os tipos da API C:

  • QkDag *, correspondente ao espaço de PythonDAGCircuit.
  • QkTarget *, correspondente ao espaço de PythonTarget.
  • QkNeighbors, um tipo de API C nativo que representa restrições de acoplamento de dois qubits.
  • QkCircuitInstruction, um tipo de API C nativo para consultar instruções individuais.

Os dois primeiros fazem parte da nossa interação com o espaço Python, mas, ao trabalhar com eles, basta considerarmos apenas a API C pura. Não há interação com o interpretador do Python neste código.

Observe que todas as funções e símbolos definidos nesta seção são declarados com static ligação. Isso ocorre porque o interpretador Python não fará a ligação com esse módulo de extensão; forneceremos ao interpretador os detalhes das funções disponíveis na próxima seção.

Não vamos nos deter nos detalhes algorítmicos desse código; é instrutivo utilizar uma etapa significativa do transpiler para a demonstração, mas a implementação exata do algoritmo não é importante para este guia.

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

Escreva o código de interação do Python

Toda a lógica de negócios está agora definida em C puro. Em seguida, é preciso conectá-lo com segurança a um Python e.

Para começar, defina a única função que será disponibilizada n Python. Isso deve seguir uma assinatura definida, que se refere exclusivamente a tipos d Python s que se assemelham a um fn(self, *args, **kwargs) método. Temos que retornar um PyObject *, que é a forma genérica de qualquer objeto Python.

A função completa fica assim:

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

Em resumo, a função:

  1. Segue uma assinatura definida para aceitar argumentos arbitrários de Python.
  2. Define o espaço para armazenar objetos nativos em C extraídos dos argumentos de Python.
  3. Chama uma função de análise para extrair os objetos nativos do C, configurada com a lista de argumentos esperados, argumentos-chave e as funções a serem usadas para convertê-los. Se isso falhar, a função propaga o erro.
  4. Delegam à lógica de negócios nativa do C da seção anterior, que altera o DAG no próprio local.
  5. Retorna o objeto PythonNone -space.

Toda a lógica mais complexa está lá dentro PyArg_ParseTupleAndKeywords. Isso está bem documentado na documentação do CPython sobre análise de argumentos, que você deve consultar para obter mais informações.

A API C do Qiskit oferece várias funções com nomes como qk_*_convert_from_python, que foram concebidas como funções de "conversão" para uso com PyArg_Parse*funções. Essas correspondem às O& chaves na string de formato; aqui, usamos qk_dag_convert_from_python e qk_target_convert_from_python. Essas funções utilizam o objeto nativo de C do argumento Python do qual derivam. Isso significa que as mutações serão propagadas para o espaço Python , mas também que você deve tomar cuidado para não liberar sua referência ao objeto Python que as suporta enquanto estiver usando o resultado. Isso é padrão na programação da API C do Python.

Em seguida, definimos as informações sobre este módulo e a função que ele contém, para que possamos passá-lo para um espaç 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,
};

Essa tabela de métodos e a estrutura de definição de módulos são descritas com mais detalhes na documentação do CPython sobre a inicialização de módulos.

Por fim, indique ao Python como inicializar o módulo. Esta é a única função no arquivo C que é exportada. Seu nome deve corresponder exatamente ao padrão PyInit_<mod>, onde <mod> é o nome do módulo (sem prefixo). Nesse caso, o nome totalmente qualificado do módulo é spectator_measures._core, e o nome não qualificado é _core, portanto, nossa função deve ser chamada PyInit__corecomo, com o duplo sublinhado.

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

Os símbolos PyMODINIT_FUNC PyModuleDef_Init e são ambos padrão na programação da API C do Python. O componente específico do Qiskit é qk_import(). É fundamental que você chame essa função durante a função de inicialização do seu módulo; você não poderá chamar nenhuma função da API C do Qiskit até que ela tenha sido executada com sucesso.


Use o pacote disponível em Python

Agora, este é um pacote completo, incluindo um módulo de extensão em C. Como foram utilizadas apenas ferramentas padrão e nenhuma biblioteca de sistema não padrão é vinculada durante a compilação, o processo de compilação é simples.

Você pode usar qualquer ferramenta de compilação do tipo “ PEP-517-compatible ”. Como exemplo básico, você pode executar o seguinte comando na raiz do repositório para instalar o pacote.

pip install .

Isso compila o módulo de extensão em C e instala o pacote completo Python no seu ambiente.

Um exemplo de uso dessa etapa personalizada do transpiler é:

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

O resultado disso é:

        ┌───┐ ░
   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
Esta página foi útil?
Relate um bug, erro de digitação ou solicite conteúdo no GitHub.