Pular para o conteúdo principal

Estender o Qiskit em Python com C

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

Este guia descreve o processo de definição de um módulo de extensão completo, configuração do seu processo de build e exposição para usuários Python. O pacote fornece um port simples de AddSpectatorMeasures dos addons do Qiskit para C. Este é um pass personalizado real com um caso de uso real nos addons do Qiskit.

dica

Os seguintes recursos externos podem ser úteis:

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

aviso

A API C do Qiskit ainda é experimental. Portanto, ainda não existe uma interface de programação ou binária totalmente estável, e pode haver mudanças incompatíveis entre versões secundárias.

Por exemplo, um módulo de extensão que usa o Qiskit v2.4.0 no momento da compilação tem garantia de funcionar com o Qiskit v2.4.1 em tempo de execução, mas pode falhar ao usar o Qiskit v2.5.0 em tempo de execução.

Requisitos

Comece em um diretório limpo.

Você deve ter a cadeia de ferramentas padrão do compilador C disponível para sua plataforma. Você também deve ter uma versão do Python que inclua seus cabeçalhos da API C (isso é padrão).

Você deve estar familiarizado com, ou preparado para consultar, as funções e objetos individuais disponíveis na API C do Qiskit. Você deve ter alguma familiaridade com programação em C.

Criar a estrutura de diretórios

Usaremos uma estrutura de diretórios baseada em src e um sistema de build simples baseado em setuptools. Estas instruções devem ser fáceis de adaptar a qualquer sistema de build que possa 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 build e 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 ao usuário e fornece algum código para interagir com os componentes do Qiskit no espaço Python.
  • src/spectator_measures/_coremodule.c define o módulo de extensão C, que conterá todo o código crítico de desempenho do nosso pacote.

Examinaremos cada arquivo em detalhes, construindo o pacote com seu módulo de extensão.

Definir os metadados do pacote

Comece definindo o arquivo pyproject.toml. Isso é padrão para um projeto baseado em setuptools, embora qiskit seja um requisito adicional no array build-system.requires, 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"}

A partir do Qiskit v2.4, a API C ainda não é estável fora de versões secundárias (por exemplo, a API C para a v2.4.0 será compatível com a v2.4.1, mas não com a v2.5.0). No futuro, pretendemos estender essa estabilidade para versões principais. Por enquanto, defina a versão de execução do Qiskit em project.dependencies para corresponder à versão secundária usada no momento da compilação.

Em muitos projetos setuptools de Python puro, seria suficiente ter apenas o arquivo pyproject.toml. No entanto, nosso módulo precisa de acesso aos arquivos de cabeçalho da API C do Qiskit durante seu processo de build. A partir da v2.4, esses arquivos estão incluídos nas distribuições Python do Qiskit SDK. Para localizar o diretório que os contém, execute qiskit.capi.get_include(). Isso resulta em um arquivo setup.py 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 é definida em pyproject.toml, e setuptools.setup() também lerá esse arquivo.

dica

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

Escrever o wrapper no espaço Python

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

Este pacote define um pass de transpilação personalizado derivado da classe Python qiskit.transpiler.TransformationPass, mas usa uma função do módulo de extensão C para toda a sua lógica de negócio. Isso tem a seguinte aparência:

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 deste pass não são importantes para este guia. Se você tiver interesse, pode consultar a documentação da API do AddSpectatorMeasures em qiskit-addon-utils. Este guia produz um port simples desse pass, sem suporte para operações de fluxo de controle.

Escrever o módulo de extensão C

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

Configurar os arquivos de cabeçalho

Ao compilar um módulo de extensão 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, você deve definir a macro QISKIT_PYTHON_EXTENSION antes de incluir qiskit.h.

Nossos includes 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>

Escrever o código puro da API C

Em seguida, escreva toda a lógica de negócio como código puro da API C do Qiskit. Vamos expor essa lógica ao espaço Python na seção seguinte.

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

  • QkDag *, correspondendo ao DAGCircuit no espaço Python.
  • QkTarget *, correspondendo ao Target no espaço Python.
  • QkNeighbors, um tipo nativo da API C que representa restrições de acoplamento entre dois qubits.
  • QkCircuitInstruction, um tipo nativo da API C para consultar instruções individuais.

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

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

Não nos deteremos nos detalhes algorítmicos deste código; é instrutivo usar um pass de transpilação significativo para a demonstração, mas a implementação precisa 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;
}

Escrever o código de interação com Python

Toda a lógica de negócio está agora definida em C puro. Em seguida, ela precisa ser exposta ao Python com segurança.

Para começar, defina a única função que será exposta ao Python. Ela deve seguir uma assinatura definida, que é puramente em termos de tipos Python com aparência de método fn(self, *args, **kwargs). Precisamos retornar um PyObject *, que é a forma genérica de qualquer objeto Python.

A função completa tem a seguinte aparência:

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 Python arbitrários.
  2. Define espaço para armazenar objetos nativos em C extraídos dos argumentos Python.
  3. Chama uma função de análise para extrair os objetos nativos em C, configurada com a lista de argumentos esperados, argumentos de palavra-chave e as funções a usar para convertê-los. Se isso falhar, a função propagará o erro.
  4. Delega para a lógica de negócio nativa em C da seção anterior, que muta o DAG no lugar.
  5. Retorna o objeto None do espaço Python.

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

A API C do Qiskit fornece várias funções com nomes como qk_*_convert_from_python, que são projetadas como funções "conversor" para uso com funções PyArg_Parse*. Elas correspondem às chaves O& na string de formato; aqui, usamos qk_dag_convert_from_python e qk_target_convert_from_python. Essas funções emprestam o objeto nativo em C do argumento Python do qual são derivadas. Isso significa que as mutações serão propagadas ao espaço Python, mas também que você deve ter cuidado para não liberar sua referência ao objeto Python que os sustenta enquanto estiver usando o resultado. Isso é padrão na programação com a API C do Python.

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

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

Por fim, informe 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 (não qualificado) do módulo. Neste caso, o nome totalmente qualificado do módulo é spectator_measures._core, e o nome não qualificado é _core, então nossa função deve se chamar PyInit__core, com o sublinhado duplo.

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 e PyModuleDef_Init são símbolos padrão da API C do Python. O componente específico do Qiskit é qk_import(). É fundamental que você chame esta função durante a função de inicialização do seu módulo; você não conseguirá chamar nenhuma função da API C do Qiskit até que isso tenha sido executado com sucesso.

Usar o pacote a partir do Python

Este é agora um pacote completo, incluindo um módulo de extensão C. Como apenas ferramentas padrão foram usadas e nenhuma biblioteca de sistema não padrão é vinculada durante o tempo de build, o processo de build é simples.

Você pode usar qualquer ferramenta de build compatível com PEP-517. Como exemplo mínimo, 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 C e instala o pacote Python completo em seu ambiente.

Um exemplo de uso deste pass de transpilação personalizado é:

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 é:

        ┌───┐ ░
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