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.
Os seguintes recursos externos podem ser úteis:
- A documentação do CPython sobre como escrever módulos de extensão.
- A documentação do NumPy sobre como usar sua API C.
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.
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.tomldefine 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.pycontém a configuração dinâmica mínima necessária para compilar nosso módulo de extensão.src/spectator_measures/__init__.pydefine 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.cdefine 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.
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
Os seguintes recursos podem ser úteis:
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 aoDAGCircuitno espaço Python.QkTarget *, correspondendo aoTargetno 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:
- Segue uma assinatura definida para aceitar argumentos Python arbitrários.
- Define espaço para armazenar objetos nativos em C extraídos dos argumentos Python.
- 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.
- Delega para a lógica de negócio nativa em C da seção anterior, que muta o DAG no lugar.
- Retorna o objeto
Nonedo 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