Princípios de design das representações de operadores
Este guia explica os princípios de design e os conceitos centrais compartilhados por todas as representações de operadores do módulo operators.
Visão geral
As representações de operadores fornecidas por este módulo compartilham vários princípios de design fundamentais:
-
Estrutura de dados esparsa: os operadores codificam apenas operações não identidade. O layout interno dos dados é, em geral, inspirado em formatos de dados de matrizes esparsas, permitindo armazenamento e computação eficientes para sistemas com muitos modos, mas relativamente poucas contribuições significativas.
-
Iteração e reconstrução de termos: independentemente do armazenamento esparso interno, os operadores oferecem uma interface de iteração consistente, para que você possa inspecionar, filtrar e transformar termos sem entender a estrutura de dados subjacente e, depois, reconstruir novos operadores a partir dos termos modificados.
-
Indexação baseada em modos: os operadores usam índices de modo abstratos para rotular os graus de liberdade fermiônicos, permitindo um mapeamento flexível de sistemas físicos para a representação do operador.
-
Agrupamento de termos e relações de comutação: os operadores oferecem suporte nativo a informações de agrupamento que associam termos a índices de grupo. Isso permite otimizações e a preservação da estrutura física sem exigir estruturas de dados separadas. Consulte o guia de agrupamento para o uso prático.
-
Operações aritméticas e matemáticas: todos os operadores implementam um conjunto consistente de operações aritméticas (como adição, multiplicação e composição) e funções matemáticas usando o protocolo
OperatorTrait, permitindo código uniforme entre diferentes tipos de operador. -
Ordenação de termos do operador e formas normais: operadores matematicamente equivalentes podem ter representações e comportamentos muito diferentes em algoritmos quânticos. Todas as representações de operadores oferecem suporte a várias formas normais (baseadas em relações de comutação específicas de cada álgebra) para obter representações de operadores canônicas, previsíveis e otimizáveis.
Estrutura de dados esparsa
Todos os operadores usam uma representação esparsa, na qual apenas as operações não identidade são rastreadas. Cada operação não identidade consiste em um coeficiente (número complexo) e uma sequência de ações em modos específicos.
Essa abordagem reduz drasticamente o uso de memória e o tempo de computação, especialmente em sistemas com muitos modos, mas relativamente poucas contribuições significativas. Ao codificar apenas operações não identidade, as operações se concentram somente no que importa, permitindo trabalhar com sistemas grandes que seriam inviáveis com representações densas. Além disso, os operadores escalam naturalmente para qualquer quantidade de modos; um operador que atua nos modos {0, 1} funciona sem alterações em sistemas com muito mais modos, pois os modos não afetados são implicitamente identidade.
Termos idênticos são preservados separadamente durante as operações aritméticas e precisam ser combinados explicitamente, se necessário.
Formato de armazenamento interno
Internamente, os operadores são armazenados em arrays inspirados em formatos de dados de matrizes esparsas:
-
Array de coeficientes: o coeficiente complexo de cada termo
-
Array de índices de modo: os modos fermiônicos sobre os quais cada ação atua
-
Array de fronteiras: índices que marcam onde começam e terminam os modos de cada termo no array de modos
Arrays adicionais podem estar presentes, dependendo do tipo de operador. Por exemplo, instâncias de FermionOperator incluem um array de ações de booleanos que especifica o tipo de ação fermiônica que atua sobre o respectivo índice de modo. Em contraste, a classe MajoranaOperator não exige essa distinção, pois codifica essa informação na paridade do índice de modo. Consulte a documentação da API do seu tipo de operador para entender o formato de armazenamento completo.
Os exemplos a seguir mostram como esses arrays são organizados. Primeiro, uma construção direta usando os arrays esparsos:
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> # Construct operators directly using sparse arrays
>>> # First operator: 1.0 * +0 -1
>>> op1 = FermionOperator(
... coeffs=[1.0],
... actions=[True, False],
... modes=[0, 1],
... boundaries=[0, 2],
... )
>>>
>>> # Second operator: 1.0 * +2 -3
>>> op2 = FermionOperator(
... coeffs=[1.0],
... actions=[True, False],
... modes=[2, 3],
... boundaries=[0, 2],
... )
>>>
>>> # Combine the sparse operators
>>> op1 += op2
>>> print(format(op1))
1.000000e0 +0.000000e0j * (+0 -1)
1.000000e0 +0.000000e0j * (+2 -3)
#include <qiskit_fermions.h>
// Construct first operator: 1.0 * +0 -1
QkComplex64 coeff1[1] = {{1.0, 0.0}};
bool actions1[2] = {true, false};
uint32_t modes1[2] = {0, 1};
uint32_t boundaries1[2] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, actions1, modes1, boundaries1);
// Construct second operator: 1.0 * +2 -3
QkComplex64 coeff2[1] = {{1.0, 0.0}};
bool actions2[2] = {true, false};
uint32_t modes2[2] = {2, 3};
uint32_t boundaries2[2] = {0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(1, 2, coeff2, actions2, modes2, boundaries2);
// Combine the sparse operators. There is no in-place add in the C API, so
// the sum is a new operator that the caller owns.
QfFermionOperator *sum = qf_ferm_op_add(op1, op2);
qf_ferm_op_free(op1);
qf_ferm_op_free(op2);
qf_ferm_op_free(sum);
Métodos de construção convenientes
Para desenvolvedores Python, há vários métodos de construção convenientes que abstraem os detalhes do armazenamento esparso. Eles facilitam a construção de operadores sem que você precise se preocupar em gerenciar os arrays de coeficientes, modos e fronteiras:
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct operators using operator algebra notation
>>> op1 = FermionOperator.from_dict({(cre(0), ann(1)): 1.0})
>>> op2 = FermionOperator.from_dict({(cre(2), ann(3)): 1.0})
>>>
>>> # The result is sparse even when combining them
>>> op1 += op2
>>> print(format(op1))
1.000000e0 +0.000000e0j * (+0 -1)
1.000000e0 +0.000000e0j * (+2 -3)
// The C API uses direct array construction; convenience methods are not available.
Implementações individuais de operadores podem oferecer métodos de construção adicionais, adequados ao seu caso de uso específico. Consulte a documentação da API do seu tipo de operador para ver todas as opções de construção disponíveis.
Iteração de termos e reconstrução
Os operadores oferecem uma interface de iteração consistente por meio de OperatorTrait.iter_terms(), independentemente de sua representação esparsa interna. Assim, você pode inspecionar, filtrar ou transformar termos sem precisar entender a estrutura de dados subjacente. Depois, você pode reconstruir um novo operador a partir dos termos transformados usando OperatorTrait.from_terms().
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct an operator with terms of different orders
>>> op = FermionOperator.from_dict({
... (): 0.5, # constant term (order 0)
... (cre(0), ann(1)): 1.0, # two-body term (order 2)
... (cre(0), cre(1), ann(1), ann(0)): 0.25 # four-body term (order 4)
... })
>>>
>>> # Filter to keep only terms of order 2
>>> order_two_terms = [
... (term, coeff) for term, coeff in op.iter_terms()
... if len(term) == 2
... ]
>>>
>>> # Reconstruct operator from filtered terms
>>> filtered_op = FermionOperator.from_terms(order_two_terms)
>>> print(f"Original operator has {len(op)} terms")
Original operator has 3 terms
>>> print(f"Filtered operator has {len(filtered_op)} term")
Filtered operator has 1 term
// WARNING: Term iteration and filtering are not yet available in the C API.
Indexação baseada em modos
Todas as representações de operadores se referem aos índices sobre os quais seus termos atuam como modos. Um modo é simplesmente um índice que identifica um grau de liberdade fermiônico no seu sistema. O mapeamento dos graus de liberdade físicos (como orbitais espaciais, estados de spin ou outros números quânticos) para os índices de modo fica a cargo do usuário, o que permite máxima flexibilidade.
Essa abstração também está presente no módulo qiskit_fermions.circuit, em que o FermionicCircuit opera sobre um registrador de modos fermiônicos. Tanto nas representações de operadores quanto nas de circuitos, os modos fornecem uma maneira consistente e independente de álgebra de especificar quais graus de liberdade participam de uma determinada operação.
As implementações atuais usam modos sem spin: todas as representações de operadores fornecidas atualmente por este módulo tratam os modos como graus de liberdade fermiônicos sem spin. Isso significa que, se o seu sistema tem elétrons ou férmions com spin para cima e spin para baixo, você deve mapeá-los explicitamente para modos distintos (por exemplo, modos 0-3 para o spin para cima de quatro orbitais espaciais, modos 4-7 para o spin para baixo, ou qualquer outra convenção que você escolher).
Esse design mantém as representações centrais simples e gerais, evitando impor uma convenção específica de ordenação de spin. Módulos utilitários como qiskit_fermions.operators.library oferecem funções de conveniência (por exemplo, FCIDump.from_file()) que cuidam desses mapeamentos para você ao carregar dados de estrutura eletrônica.
À medida que o pacote evolui, podem ser adicionadas representações de operadores com spin que ofereçam suporte nativo a graus de liberdade de spin em seu modelo de dados. Elas serão claramente distinguidas das implementações atuais sem spin e coexistirão com elas no módulo.
Agrupamento de termos e relações de comutação
Assim como os coeficientes e os índices de modo, os operadores podem opcionalmente armazenar um array de grupos que associa cada termo a um índice de grupo. Ao integrar o agrupamento diretamente à representação do operador, como parte da estrutura de dados esparsa, a informação de agrupamento acompanha naturalmente o operador ao longo das transformações. Isso permite usar a estrutura de forma sistemática, seja ela proveniente de propriedades físicas, relações algébricas ou simetrias específicas do problema. A informação estruturada pode então ser usada em operações posteriores, como síntese e decomposição de circuitos, por meio de métodos como OperatorTrait.split_out_groups().
Para orientações detalhadas sobre como agrupar termos de operadores nos seus fluxos de trabalho, consulte o guia de agrupamento.
- PYTHON
- C
>>> from qiskit_fermions.operators import MajoranaOperator, gamma
>>> op = MajoranaOperator.from_dict({
... (gamma(0, False),): 1.0,
... (gamma(1, False),): 1.0,
... (gamma(2, False), gamma(3, False)): 1.0
... })
>>> # Assign group indices to terms
>>> op.groups = [0, 0, 1]
>>> # Partition operator by groups
>>> grouped_ops = op.split_out_groups()
#include <qiskit_fermions.h>
// Create operator with 3 terms
QkComplex64 coeffs[3] = {{1.0, 0.0}, {1.0, 0.0}, {1.0, 0.0}};
uint32_t modes[4] = {0, 1, 2, 3};
uint32_t boundaries[4] = {0, 1, 2, 4};
QfMajoranaOperator *op = qf_maj_op_new(3, 4, coeffs, modes, boundaries);
// Assign grouping information
uint32_t groups[3] = {0, 0, 1};
qf_maj_op_set_groups(op, groups, 3);
// Partition operator by groups
QfMajoranaOperator *grouped_ops[2];
qf_maj_op_split_out_groups(op, NULL, 0, grouped_ops);
Operações aritméticas e matemáticas
Todos os operadores implementam o protocolo OperatorTrait, que fornece um conjunto unificado de operações para diferentes tipos de operadores. Isso garante que o código escrito para uma representação de operador funcione de maneira uniforme com as outras. Consulte qiskit_fermions.protocols para ver como esse protocolo se relaciona com os demais definidos por este pacote.
O protocolo inclui operações aritméticas (como adição, multiplicação e composição), operações estruturais (iteração de termos, análise de suporte de modos, renomeação), funções matemáticas (ordenação normal, simplificação, verificação de equivalência) e muito mais. Consulte a documentação de OperatorTrait para uma referência completa de todas as operações disponíveis.
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct a Hermitian operator: H = +0 -1 + +1 -0
>>> op = FermionOperator.from_dict({
... (cre(0), ann(1)): 1.0,
... (cre(1), ann(0)): 1.0
... })
>>>
>>> # Check if the operator is Hermitian
>>> print(f"Operator is Hermitian: {op.is_hermitian(atol=1e-10)}")
Operator is Hermitian: True
#include <qiskit_fermions.h>
// Construct a Hermitian operator: H = +0 -1 + +1 -0
QkComplex64 coeffs[2] = {{1.0, 0.0}, {1.0, 0.0}};
bool actions[4] = {true, false, true, false};
uint32_t modes[4] = {0, 1, 1, 0};
uint32_t boundaries[3] = {0, 2, 4};
QfFermionOperator *op = qf_ferm_op_new(2, 4, coeffs, actions, modes, boundaries);
// Check if the operator is Hermitian
bool is_hermitian = qf_ferm_op_is_hermitian(op, 1e-10);
printf("Operator is Hermitian: %s\n", is_hermitian ? "true" : "false");
// Clean up
qf_ferm_op_free(op);
is_hermitian() verifica que reduzindo essa diferença a uma forma normal e comparando-a com zero(). Detalhado em termos das outras operações do protocolo, ele realiza o seguinte:
>>> difference = (op - op.adjoint()).normal_ordered().simplify(atol=1e-10)
>>> difference.equiv(FermionOperator.zero(), atol=1e-10)
True
A força dessa verificação depende do tipo de operador, pois depende de quão completamente a forma normal desse tipo contrai geradores adjacentes. Um resultado True é sempre confiável, mas, para alguns tipos, um resultado False é conservador: consulte is_hermitian() para mais detalhes.
O exemplo usa atol=1e-10. O parâmetro atol (tolerância absoluta) especifica um limite. Coeficientes com magnitude menor que atol são tratados como zero e descartados. Isso é essencial para a estabilidade numérica ao comparar operadores, pois a aritmética de ponto flutuante pode introduzir pequenos erros de arredondamento que, de outra forma, impediriam que operadores equivalentes fossem reconhecidos como tais.
Embora o protocolo OperatorTrait ofereça uma interface comum, implementações individuais de operadores podem oferecer métodos de conveniência adicionais que não fazem parte do protocolo. Por exemplo, FermionOperator e MajoranaOperator oferecem um método max_rank() que informa o comprimento do seu maior termo. Sempre consulte a documentação da API do seu tipo de operador para descobrir todas as funcionalidades disponíveis.
Ordenação de termos de operadores e formas normais
Um desafio fundamental da álgebra de operadores quânticos é que operadores matematicamente equivalentes podem ser representados de muitas maneiras diferentes, cada uma com implicações distintas para os algoritmos quânticos. O mesmo operador pode ser escrito em formas algebricamente equivalentes (por exemplo, pode ser expresso como ), mas essas representações levam a comportamentos diferentes na síntese de circuitos, na simplificação e em algoritmos numéricos.
As representações de operadores oferecem suporte a operações de ordenação normal, que transformam operadores em formas canônicas específicas de cada álgebra usando relações de comutação. Isso permite comparações confiáveis (dois operadores equivalentes têm formas normalmente ordenadas idênticas), revela simplificações (as relações de comutação fazem termos se cancelarem ou se combinarem) e dá suporte a algoritmos que exigem formas específicas de operadores para garantir correção e eficiência.
Diferentes representações de operadores podem implementar a ordenação normal com base em diferentes relações de comutação, apropriadas à sua álgebra. Por exemplo, a ordenação normal fermiônica usa relações de anticomutação (), enquanto a ordenação normal de Majorana usa convenções algébricas diferentes (). Sempre consulte a documentação do seu tipo de operador para entender como a ordenação normal é implementada.
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Two different representations of the same operator
>>> op1 = FermionOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> op2 = FermionOperator.from_dict({(): 1.0, (cre(0), ann(0)): -1.0})
>>>
>>> # Direct comparison fails due to different forms
>>> op1.equiv(op2, atol=1e-10)
False
>>>
>>> # Normal-order both and compare again
>>> op1_normal = op1.normal_ordered()
>>> op2_normal = op2.normal_ordered()
>>> op1_normal.equiv(op2_normal, atol=1e-10)
True
#include <qiskit_fermions.h>
#include <stdbool.h>
// Two different representations of the same operator
QkComplex64 coeff1[1] = {{1.0, 0.0}};
bool actions1[2] = {false, true};
uint32_t modes1[2] = {0, 0};
uint32_t boundaries1[2] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, actions1, modes1, boundaries1);
QkComplex64 coeff2[2] = {{1.0, 0.0}, {-1.0, 0.0}};
bool actions2[2] = {true, false};
uint32_t modes2[2] = {0, 0};
uint32_t boundaries2[3] = {0, 0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(2, 2, coeff2, actions2, modes2, boundaries2);
// Direct comparison fails due to different forms
bool equiv_before = qf_ferm_op_equiv(op1, op2, 1e-10);
printf("Equivalent before normal ordering: %s\n", equiv_before ? "true" : "false");
// Normal-order both and compare again
QfFermionOperator *op1_normal = qf_ferm_op_normal_ordered(op1, NULL);
QfFermionOperator *op2_normal = qf_ferm_op_normal_ordered(op2, NULL);
bool equiv_after = qf_ferm_op_equiv(op1_normal, op2_normal, 1e-10);
printf("Equivalent after normal ordering: %s\n", equiv_after ? "true" : "false");
// Clean up
qf_ferm_op_free(op1);
qf_ferm_op_free(op2);
qf_ferm_op_free(op1_normal);
qf_ferm_op_free(op2_normal);
O método de protocolo OperatorTrait.normal_ordered() deixa deliberadamente não especificados seus argumentos posicionais e nomeados, permitindo que implementações concretas de operadores definam parâmetros ajustáveis que controlam a forma canônica exata produzida. Isso possibilita otimizações específicas de cada operador e variantes de forma normal adaptadas à sua álgebra ou ao seu caso de uso. Consulte a documentação da API do seu tipo de operador para ver quais parâmetros estão disponíveis.