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.
Os seguintes recursos externos podem ser úteis para você:
- A documentação do CPython sobre como escrever módulos de extensão.
- A documentação do NumPy sobre como usar sua API em C.
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.
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
Pythonque 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.cEm resumo:
pyproject.tomldefine 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.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 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.cdefine 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.
[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:
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.
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:
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 dagOs 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
Os seguintes recursos podem ser úteis para você:
- A documentação do CPython sobre como escrever módulos de extensão.
- A documentação do NumPy sobre como usar sua API em C.
- [Referência da API C] c-apido Qiskit.
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:
#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.
/**
* 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:
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 arbitrários de
Python. - Define o espaço para armazenar objetos nativos em C extraídos dos argumentos de
Python. - 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.
- Delegam à lógica de negócios nativa do C da seção anterior, que altera o DAG no próprio local.
- 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 :
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.
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