Princípios de projeto das representações de operadores
Este guia explica os princípios comuns de projeto e os conceitos fundamentais presentes em todas as representações de operadores no operators módulo.
Visão geral
As representações de operadores fornecidas por este módulo compartilham vários princípios fundamentais de projeto:
- Estrutura de dados esparsa : os operadores codificam apenas operações que não sejam de identidade. A estrutura interna dos dados é, em geral, inspirada nos formatos de dados de matrizes esparsas, permitindo armazenamento e computação eficientes para sistemas com muitos modos, mas com relativamente poucas contribuições significativas.
- Iteração e reconstrução de termos : Independentemente do armazenamento interno esparso, os operadores oferecem uma interface de iteração consistente, permitindo que você inspecione, filtre e transforme termos sem precisar compreender a estrutura de dados subjacente e, em seguida, reconstrua novos operadores a partir dos termos modificados.
- Indexação baseada em modos : os operadores utilizam índices de modos abstratos para designar os graus de liberdade fermiónicos, permitindo um mapeamento flexível dos sistemas físicos para a representação por operadores.
- Agrupamento de termos e relações de comutação : Os operadores oferecem suporte nativo ao agrupamento de informações que associam termos a índices de grupo. Isso permite otimizações e a preservação da estrutura física sem a necessidade de estruturas de dados separadas. Consulte o guia de agrupamento para obter orientações práticas sobre como utilizá-lo.
- 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 por meio do protocolo
OperatorTrait, permitindo a uniformidade do código entre diferentes tipos de operadores. - Ordenação dos operadores e formas normais : Operadores matematicamente equivalentes podem ter representações e comportamentos muito diferentes em algoritmos quânticos. Todas as representações de operadores suportam várias formas normais (baseadas em relações de comutação específicas da álgebra) para obter representações de operadores canônicas, previsíveis e otimizáveis.
Estrutura de dados esparsa
Todos os operadores utilizam uma representação esparsa, na qual apenas as operações que não sejam de identidade são rastreadas. Cada operação não identitária 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 com relativamente poucas contribuições significativas. Ao codificar apenas operações que não sejam de identidade, as operações se concentram apenas no que é essencial, permitindo trabalhar com sistemas de grande porte, o que seria inviável com representações densas. Além disso, os operadores se adaptam naturalmente a qualquer número de modos; um operador que atua sobre modos funciona {0, 1} da mesma forma em sistemas com um número muito maior de modos, uma vez que os modos não afetados representam implicitamente a identidade.
Termos idênticos são mantidos separadamente durante as operações aritméticas e devem ser combinados explicitamente, se necessário.
Formato de armazenamento interno
Internamente, os operadores são armazenados em matrizes inspiradas nos formatos de dados de matrizes esparsas:
- Matriz de coeficientes : o coeficiente complexo de cada termo
- Matriz de índices de modos : os modos fermiónicos sobre os quais cada ação atua
- Matriz de limites : índices que indicam onde os modos de cada termo começam e terminam na matriz de modos
Podem existir matrizes adicionais, dependendo do tipo de operador. Por exemplo, as instâncias FermionOperator incluem uma matriz de ações composta por valores booleanos que especifica o tipo de ação fermiónica que atua no respectivo índice de modo. Em contrapartida, a classe MajoranaOperator não exige essa distinção, uma vez que codifica essa informação na paridade do índice do modo. Consulte a documentação da API referente ao seu tipo de operador para entender o formato completo de armazenamento.
Os exemplos a seguir mostram como esses arranjos são organizados. Primeiro, temos uma construção direta utilizando os vetores esparsos:
[x] PYTHON
>>> 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)[] C
#include <qiskit_fermions.h>
// Construct first operator: 1.0 * c_0 a_1
QkComplex67 coeff1[1] = {{1.0, 0.0}};
uint32_t modes1[2] = {0, 1};
uint32_t boundaries1[2] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, modes1, boundaries1);
// Construct second operator: 1.0 * c_2 a_3
QkComplex67 coeff2[1] = {{1.0, 0.0}};
uint32_t modes2[2] = {2, 3};
uint32_t boundaries2[2] = {0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(1, 2, coeff2, modes2, boundaries2);
// Add operators
qf_ferm_op_add_assign(op1, op2);
qf_ferm_op_free(op1);
qf_ferm_op_free(op2);Métodos de construção práticos
Para desenvolvedores d Python, estão disponíveis vários métodos de construção práticos que abstraem os detalhes do armazenamento esparso. Isso facilita a criação de operadores sem a necessidade de se preocupar com o gerenciamento de matrizes de coeficientes, modos e condições de contorno:
[x] PYTHON
>>> 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)[] C
// The C API uses direct array construction; convenience methods are not available.Implementações específicas de operadores podem oferecer métodos de construção adicionais adequados ao seu caso de uso específico. Consulte a documentação da API referente ao seu tipo de operador para ver todas as opções de construção disponíveis.
Iteração e reconstrução de termos
Os operadores oferecem uma interface de iteração consistente ao utilizarem independentemente OperatorTrait.iter_terms() de sua representação esparsa interna. Portanto, você pode inspecionar, filtrar ou transformar termos sem precisar entender a estrutura de dados subjacente. É possível, então, reconstruir um novo operador a partir dos termos transformados utilizando OperatorTrait.from_terms().
[x] PYTHON
>>> 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[] C
// WARNING: Term iteration and filtering are not yet available in the C API.Indexação baseada em modo
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 em seu sistema. A correspondência entre os graus de liberdade físicos (como orbitais espaciais, estados de spin ou outros números quânticos) e os índices de modo fica a critério do usuário, proporcionando o máximo de flexibilidade.
Essa abstração também está presente no módulo qiskit_fermions.circuit , onde o FermionicCircuit opera sobre um registro de modos fermiónicos. Tanto na representação por operadores quanto na representação por circuitos, os modos oferecem uma maneira consistente e independente da álgebra de especificar quais graus de liberdade participam de uma determinada operação.
As implementações atuais utilizam modos sem spin : todas as representações de operadores atualmente fornecidas por este módulo tratam os modos como graus de liberdade fermiónicos sem spin. Isso significa que, se o seu sistema tiver elétrons ou férmions tanto com spin para cima quanto com spin para baixo, você deverá atribuí-los explicitamente a modos distintos (por exemplo, modos 0 a 3 para o spin para cima de quatro orbitais espaciais, modos 4 a 7 para o spin para baixo, ou qualquer outra convenção que você escolher).
Esse projeto mantém as representações centrais simples e gerais, evitando, ao mesmo tempo, impor uma convenção específica de ordenação de spins. Módulos utilitários como fornecem qiskit_fermions.operators.library 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 for evoluindo, poderão ser adicionadas representações de operadores de spin que ofereçam suporte nativo aos graus de liberdade de spin em seu modelo de dados. Elas serão claramente diferenciadas 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 uma matriz de grupos que associa cada termo a um índice de grupo. Ao integrar o agrupamento diretamente na representação do operador como parte da estrutura de dados esparsa, as informações de agrupamento acompanham naturalmente o operador ao longo das transformações. Isso permite o uso sistemático de estruturas, seja a partir de propriedades físicas, relações algébricas ou simetrias específicas do problema. As informações estruturadas podem, então, ser utilizadas em operações posteriores, como síntese e decomposição de circuitos, por meio de métodos como OperatorTrait.split_out_groups().
Para obter orientações detalhadas sobre como agrupar termos de operadores em seus fluxos de trabalho, consulte o guia de agrupamento.
[x] PYTHON
>>> 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()[] C
#include <qiskit_fermions.h>
// Create operator with 3 terms
QkComplex67 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
Todas as operadoras implementam o protocolo OperatorTrait , que oferece um conjunto unificado de operações para diferentes tipos de operadoras. Isso garante que o código escrito para uma representação de operador funcione de maneira uniforme com as demais. Consulte qiskit_fermions.protocols para saber como esse protocolo se relaciona com os demais definidos neste 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 modo, reetiquetagem), funções matemáticas (ordenação normal, simplificação, verificação de equivalência) e muito mais. Consulte a documentação OperatorTrait para obter uma referência completa de todas as operações disponíveis.
[x] PYTHON
>>> 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 by verifying H - H† = 0
>>> adjoint = op.adjoint()
>>> difference = op - adjoint
>>> difference = difference.normal_ordered()
>>> difference = difference.simplify(atol=1e-10)
>>> is_hermitian = difference.equiv(FermionOperator.zero(), atol=1e-10)
>>> print(f"Operator is Hermitian: {is_hermitian}")
Operator is Hermitian: True[] C
#include <qiskit_fermions.h>
// Construct a Hermitian operator: H = +0 -1 + +1 -0
QkComplex67 coeffs[2] = {{1.0, 0.0}, {1.0, 0.0}};
uint32_t modes[4] = {0, 1, 1, 0};
uint32_t boundaries[3] = {0, 2, 4};
QfFermionOperator *op = qf_ferm_op_new(2, 4, coeffs, modes, boundaries);
// Check if Hermitian: compute H - H†, normal-order, and simplify
QfFermionOperator *adjoint = qf_ferm_op_adjoint(op);
QfFermionOperator *difference = qf_ferm_op_sub(op, adjoint);
QfFermionOperator *normal_ordered = qf_ferm_op_normal_ordered(difference);
qf_ferm_op_ichop(normal_ordered, 1e-10);
QfFermionOperator *zero = qf_ferm_op_zero();
bool is_hermitian = qf_ferm_op_equiv(normal_ordered, zero, 1e-10);
printf("Operator is Hermitian: %s\n", is_hermitian ? "true" : "false");
// Clean up
qf_ferm_op_free(op);
qf_ferm_op_free(adjoint);
qf_ferm_op_free(difference);
qf_ferm_op_free(normal_ordered);
qf_ferm_op_free(zero);O exemplo utiliza tanto atol=1e-10 em quanto simplify() em equiv(). O parâmetro ( atol tolerância absoluta) define um limite. Os coeficientes cuja magnitude seja menor que atol são considerados nulos e descartados. Isso é essencial para a estabilidade numérica ao comparar operadores, já que a aritmética de ponto flutuante pode introduzir pequenos erros de arredondamento que, de outra forma, impediriam que operadores equivalentes fossem reconhecidos como tal.
Embora o protocolo OperatorTrait forneça uma interface comum, as implementações específicas de cada operador podem oferecer métodos de conveniência adicionais que não fazem parte do protocolo. Por exemplo, alguns operadores oferecem um método is_hermitian() que implementa essa verificação. Sempre consulte a documentação da API correspondente ao seu tipo de operador para conhecer todas as funcionalidades disponíveis.
Ordenação de termos de operadores e formas normais
Um desafio fundamental na á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 ); no entanto, essas representações levam a comportamentos diferentes na síntese de circuitos, na simplificação e nos algoritmos numéricos.
As representações dos operadores suportam operações de ordenação normal que transformam os operadores em formas canônicas específicas da álgebra, utilizando relações de comutação. Isso permite comparações confiáveis (dois operadores equivalentes têm formas ordenadas normalmente idênticas), revela simplificações (as relações de comutação fazem com que termos sejam cancelados ou combinados) e viabiliza algoritmos que exigem formas específicas de operadores para garantir a correção e a eficiência.
Diferentes representações de operadores podem implementar a ordenação normal com base em relações de comutação distintas, adequadas à respectiva álgebra. Por exemplo, a ordenação normal fermiónica utiliza relações de anticomutação ( ), enquanto a ordenação normal de Majorana utiliza convenções algébricas diferentes ( ). Consulte sempre a documentação do seu tipo de operador para entender como a ordenação normal é implementada.
[x] PYTHON
>>> 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[] C
#include <qiskit_fermions.h>
#include <stdbool.h>
// Two different representations of the same operator
QkComplex67 coeff1[1] = {{1.0, 0.0}};
uint32_t modes1[2] = {0, 0};
uint32_t boundaries1[3] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, modes1, boundaries1);
QkComplex67 coeff2[2] = {{1.0, 0.0}, {-1.0, 0.0}};
uint32_t modes2[2] = {0, 0};
uint32_t boundaries2[3] = {0, 0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(2, 2, coeff2, 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);
QfFermionOperator *op2_normal = qf_ferm_op_normal_ordered(op2);
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 sem especificação seus argumentos posicionais e de palavra-chave, permitindo que as implementações concretas do operador definam parâmetros ajustáveis que controlam a forma canônica exata produzida. Isso permite otimizações específicas para cada operador e variantes da forma normal adaptadas à sua álgebra ou ao seu caso de uso. Consulte a documentação da API referente ao seu tipo de operador para verificar quais parâmetros estão disponíveis.