Pular para o conteúdo principal
Este projeto está em fase beta. Encontrou um bug? Abra uma issue no GitHub. Tem uma pergunta ou ideia? Pergunte no Discussions — adoraríamos receber seu feedback!
Como está a qualidade da tradução nesta página?

Princípios de design das representações de operadores

Este guia explica como um BosonOperator é organizado e como trabalhar com ele: como construir um, como inspecionar e combinar operadores e como levar dois operadores a uma forma em que possam ser comparados. Comece aqui quando você escrever um Hamiltoniano.

Um BosonOperator é uma soma exata de produtos de operadores bosônicos de criação e aniquilação com coeficientes complexos. Ele não carrega nenhum corte de ocupação. Um modo bosônico comporta um número arbitrário de bósons, então seu espaço de Fock tem dimensão infinita, e uma truncagem só entra quando o operador é mapeado para qubits (veja Trunque o espaço de Fock). Tudo neste guia acontece, portanto, na álgebra exata das relações canônicas de comutação,

[bi,bj†]=δij,[bi,bj]=[bi†,bj†]=0 .[b_i, b_j^\dagger] = \delta_{ij}, \qquad [b_i, b_j] = [b_i^\dagger, b_j^\dagger] = 0 \, .

Duas consequências moldam a interface. Um índice de modo pode se repetir dentro de um único termo, porque bi†bi†b_i^\dagger b_i^\dagger é um operador válido que cria dois bósons. Além disso, reordenar dois fatores nunca introduz um sinal, embora mover um operador de aniquilação para além de um operador de criação no mesmo modo adicione um termo, bibi†=bi†bi+1b_i b_i^\dagger = b_i^\dagger b_i + 1.

Armazenamento​

Um operador mantém seus termos em quatro arrays planos, semelhantes ao formato de linha esparsa comprimida de uma matriz esparsa:

coeffsUm coeficiente complexo por termo.
actionsUm booleano por fator: True para um operador de criação e False para um de aniquilação.
modesUm índice de modo por fator.
boundariesAs posições em actions e modes onde cada termo começa e termina.

O construtor recebe esses quatro arrays diretamente. O operador abaixo tem três termos, a saber 2b0†b12 b_0^\dagger b_1, −ib1†b1†-i b_1^\dagger b_1^\dagger e a constante 0.50.5, e você pode ler os arrays de volta com os métodos get_*:

>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>> op = BosonOperator(
... coeffs=[2.0, -1.0j, 0.5],
... actions=[True, False, True, True],
... modes=[0, 1, 1, 1],
... boundaries=[0, 2, 4, 4],
... )
>>> op.get_coeffs()
[(2+0j), (-0-1j), (0.5+0j)]
>>> op.get_actions()
[True, False, True, True]
>>> op.get_modes()
[0, 1, 1, 1]
>>> op.get_boundaries()
[0, 2, 4, 4]
>>> len(op)
3

O primeiro termo abrange as posições 0 a 2 de actions e modes, o segundo abrange de 2 a 4, e o terceiro abrange de 4 a 4, que é como o termo constante, um termo sem fatores, é armazenado. O modo repetido 1 no segundo termo é preservado como foi escrito; um termo nunca é descartado nem mesclado porque um índice de modo aparece duas vezes.

Nota

Um operador construído com from_dict() não armazena seus termos na ordem do dicionário, então seus arrays podem listar os termos em uma ordem diferente a cada execução. O operador em si é o mesmo, e nada abaixo que compare ou imprima operadores depende da ordem armazenada.

O layout torna a construção e a aritmética baratas, com uma consequência: um operador pode conter o mesmo termo várias vezes ao mesmo tempo. Somas e produtos acrescentam termos e não os mesclam, então chame simplify() quando precisar de termos únicos (veja Faça aritmética).

Construa operadores​

A forma mais direta de construir um operador é from_dict(), que recebe um dicionário de uma tupla de fatores para um coeficiente. As funções cre() e ann() criam os fatores, e a tupla vazia é o termo constante:

>>> b = BosonOperator.from_dict({(ann(0),): 1.0})
>>> b_dag = BosonOperator.from_dict({(cre(0),): 1.0})
>>> number = BosonOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> print(format(number))
1.000000e0 +0.000000e0j * (+0 -0)

Na forma impressa, +0 representa b0†b_0^\dagger e -0 representa b0b_0. Os construtores zero() e one() retornam a identidade aditiva e a multiplicativa, e from_terms() reconstrói um operador a partir do iterador de iter_terms().

A biblioteca de operadores fornece as quadraturas adimensionais de posição e momento,

q^=b†+b2,p^=i(b†−b)2 ,\hat{q} = \frac{b^\dagger + b}{\sqrt{2}}, \qquad \hat{p} = \frac{i \left(b^\dagger - b\right)}{\sqrt{2}} \, ,

como position() e momentum(). Junto com as funções de comutador do mesmo módulo, você pode verificar a álgebra diretamente:

>>> from qiskit_bosons.operators.library import commutator, momentum, position
>>> q, p = position(0), momentum(0)
>>> q.is_hermitian() and p.is_hermitian()
True
>>> commutator(q, p).equiv(1j * BosonOperator.one())
True
>>> commutator(b, b_dag).equiv(BosonOperator.one())
True

O módulo também fornece anti_commutator() e double_commutator(). Os três recebem operadores do mesmo tipo e retornam um novo operador.

Atenção à ordem de um produto​

Dois operadores podem ser multiplicados de duas formas, e é fácil confundi-las. O operador @ é o produto matemático, então a @ b é ABAB. O operador & é a composição, que segue a convenção do Qiskit de leitura da esquerda para a direita: a & b aplica primeiro a e depois b, então é BABA. As funções de comutador usam @, então commutator(a, b) é [A,B][A, B]. Construir o colchete você mesmo com & o nega silenciosamente:

>>> (b @ b_dag).equiv(b_dag @ b + BosonOperator.one())
True
>>> (b & b_dag).equiv(b_dag @ b)
True
>>> ((b @ b_dag) - (b_dag @ b)).equiv(BosonOperator.one())
True
>>> ((b & b_dag) - (b_dag & b)).equiv(-BosonOperator.one())
True

Inspecione um operador​

Um operador não é diretamente iterável, porque seus termos ficam nos arrays planos acima. Use iter_terms() em vez disso, que produz uma lista de pares (is_creation, mode) e um coeficiente para cada termo. O iterador retorna cópias, então alterá-las não altera o operador.

>>> hopping = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(2), cre(2), ann(2), ann(2)): 0.5,
... }
... )
>>> len(hopping)
3
>>> sorted(hopping.iter_terms())
[([(True, 0), (False, 1)], (-1+0j)), ([(True, 1), (False, 0)], (-1+0j)), ([(True, 2), (True, 2), (False, 2), (False, 2)], (0.5+0j))]

Vários métodos resumem a estrutura de um operador:

>>> hopping.get_support()
{0, 1, 2}
>>> hopping.min_num_modes()
3
>>> hopping.max_rank()
4

get_support() é o conjunto de modos em que o operador atua, min_num_modes() é o menor registrador que pode contê-lo, e max_rank() é o comprimento de seu termo mais longo.

Nota

Um modo é um índice abstrato que rotula um grau de liberdade bosônico. Ele não tem significado próprio: você determina qual sítio de uma rede, qual coordenada vibracional ou qual porta óptica um modo representa. O BosonicCircuit usa os mesmos índices (veja Trabalhe com circuitos bosônicos), então um operador e as portas que o carregam concordam sobre o que é o modo 2 sem que nenhum dos dois saiba o que ele representa. Nada no operador fixa quantos níveis um modo tem, e é também por isso que o tamanho do registrador é apenas um limite inferior; min_num_modes() é o menor registrador que contém o operador, não uma propriedade dele.

Os métodos de ocupação informam quantos bósons um único termo pode colocar em um modo. Esse é um limite inferior para o corte de que um mapeamento precisa. Não é um corte suficiente, porque aplicações repetidas do operador, como em uma evolução temporal, podem povoar níveis mais altos (veja Trunque o espaço de Fock):

>>> hopping.mode_occupations()
[1, 1, 2]
>>> hopping.mode_occupations([2, 0])
[2, 1]
>>> hopping.max_occupation()
2

Duas verificações adicionais examinam quantidades conservadas. Um operador conserva o número de partículas quando cada termo tem tantos operadores de criação quanto de aniquilação. Passar uma lista de tamanhos de bloco testa a mesma condição dentro de cada bloco de modos consecutivos:

>>> hopping.conserves_particle_number()
True
>>> move = BosonOperator.from_dict({(cre(0), ann(2)): 1.0})
>>> move.conserves_sector([3])
True
>>> move.conserves_sector([2, 1])
False

Por fim, relabel_modes() renomeia modos: o modo i se torna permutation[i]. A lista deve ser longa o bastante para indexar todos os modos do operador e não deve repetir nenhuma entrada:

>>> print(format(move.relabel_modes([5, 6, 7])))
1.000000e0 +0.000000e0j * (+5 -7)

Faça aritmética​

Operadores suportam adição, subtração, negação e multiplicação e divisão por um número, na forma usual de operador ou in-place:

>>> total = 2 * b_dag + b_dag - 3j * b
>>> len(total)
3
>>> len(total.simplify())
2
>>> total.simplify().equiv(3 * b_dag - 3j * b)
True

Os três termos de total nunca foram mesclados, então o operador continha b_dag duas vezes até que simplify() os combinasse. O método soma os coeficientes de termos idênticos e descarta aqueles cuja magnitude total está abaixo de uma tolerância. É uma operação estrutural: ele compara os fatores escritos e não aplica as relações de comutação. O método relacionado ichop() remove coeficientes pequenos in-place sem mesclar antes, então chame simplify() antes de ichop() quando um operador puder conter termos duplicados.

Produtos usam @ ou & como descrito acima, e ** eleva um operador a uma potência inteira:

>>> x = b_dag + b
>>> print(format((x ** 2).simplify()))
1.000000e0 +0.000000e0j * (-0 -0)
1.000000e0 +0.000000e0j * (-0 +0)
1.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0)

O resultado lista as quatro ordenações como foram escritas. Reduzi-las a uma única forma exige as relações de comutação, que a próxima seção aplica.

O adjunto inverte a ordem dos fatores em cada termo, troca operadores de criação e aniquilação e conjuga os coeficientes. O método is_hermitian() testa se um operador é igual ao seu adjunto:

>>> hop = BosonOperator.from_dict({(cre(0), ann(1)): 1j})
>>> print(format(hop.adjoint()))
0.000000e0 -1.000000e0j * (+1 -0)
>>> hop.is_hermitian()
False
>>> (hop + hop.adjoint()).is_hermitian()
True

Um termo de salto isolado não é hermitiano, o que importa quando você agrupa termos para uma evolução temporal: cada grupo deve ser hermitiano por si só (veja Agrupe termos de operadores: use a estrutura do operador).

Ordem normal e comparação de operadores​

Dois produtos de operadores de escada podem ser o mesmo operador e ainda assim parecer diferentes, porque b b†b\,b^\dagger e b†b+1b^\dagger b + 1 são iguais na álgebra. Operadores que são iguais nesse sentido não precisam ser armazenados de forma igual, então compará-los exige as relações de comutação. O método normal_ordered() as aplica: ele move todo operador de criação para a esquerda de todo operador de aniquilação e adiciona um termo sempre que um par no mesmo modo é trocado.

>>> product = BosonOperator.from_dict({(ann(0), cre(0), ann(0), cre(0)): 1.0})
>>> ordered = product.normal_ordered()
>>> len(ordered)
5
>>> print(format(ordered.simplify()))
1.000000e0 +0.000000e0j * ()
3.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0 -0 -0)

O operador ordenado é retornado como uma soma não mesclada, com cinco termos armazenados que se reduzem a três depois que simplify() os mescla. A ordenação normal nunca faz um termo desaparecer: b†b†b^\dagger b^\dagger e todo outro fator repetido permanecem.

O argumento sandwich define como os modos dentro dos dois grupos de fatores são organizados. Por padrão, ambos os grupos são ordenados em ordem decrescente. Com sandwich=True, os operadores de criação ficam em ordem crescente e os de aniquilação em ordem decrescente, o que coloca os índices maiores em direção ao meio:

>>> pair = BosonOperator.from_dict({(cre(0), cre(1), ann(0), ann(1)): 1.0})
>>> list(pair.normal_ordered().iter_terms())
[([(True, 1), (True, 0), (False, 1), (False, 0)], (1+0j))]
>>> list(pair.normal_ordered(sandwich=True).iter_terms())
[([(True, 0), (True, 1), (False, 1), (False, 0)], (1+0j))]

O operador == testa a igualdade exata dos termos armazenados, sem tolerância e sem simplificação, então pode relatar uma diferença entre dois operadores que são matematicamente iguais. O método equiv() leva primeiro a diferença dos dois operadores à ordem normal e depois testa se todo coeficiente está abaixo de uma tolerância. É a comparação que você quase sempre quer:

>>> reordered = BosonOperator.from_dict({(cre(0), ann(0)): 1.0, (): 1.0})
>>> contracted = BosonOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> contracted == reordered
False
>>> contracted.simplify() == reordered.simplify()
False
>>> contracted.equiv(reordered)
True

O método simplify sozinho mantém as duas formas separadas, porque mescla apenas termos escritos idênticos. Da mesma forma, is_hermitian() compara um operador com seu adjunto em ordem normal, então reconhece b b†+b†bb\,b^\dagger + b^\dagger b como hermitiano sem trabalho adicional.

Nota

A ordenação normal é uma identidade exata na álgebra não truncada, e equiv depende disso. Ela não é uma identidade após uma truncagem. Um operador mapeado depende da forma que você mapeia, então as imagens mapeadas de contracted e reordered acima diferem, apenas no nível retido mais alto. Veja Trunque o espaço de Fock para o tamanho da diferença e como escolher uma forma.

Ordene e filtre termos​

Reordenar os termos de um operador, em oposição aos fatores dentro de um termo, não altera o operador, mas pode alterar o comportamento de algoritmos que leem os termos em ordem, como uma fórmula de produto aleatorizada. As funções de qiskit_bosons.operators.terms retornam um novo operador e mantêm quaisquer índices de grupo associados aos seus termos.

canonical_order() ordena os termos em uma ordem fixa que depende apenas dos fatores escritos, então um operador é lido da mesma forma, não importa como foi montado. order_terms() ordena por uma chave que você fornece, que recebe cada termo como a tupla que iter_terms() produz:

>>> from qiskit_bosons.operators.terms import canonical_order, order_terms
>>> mixed = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> list(canonical_order(mixed).iter_terms())
[([], (2+0j)), ([(True, 0), (False, 1)], (3+0j)), ([(True, 1), (False, 0)], (1+0j))]
>>> by_weight = order_terms(mixed, key=lambda term: abs(term[1]), reverse=True)
>>> [coeff for _, coeff in by_weight.iter_terms()]
[(3+0j), (2+0j), (1+0j)]

Se um operador carrega índices de grupo (veja Agrupe termos de operadores: use a estrutura do operador), a função group_order() faz de cada grupo uma sequência contígua de termos:

>>> from qiskit_bosons.operators.terms import group_order
>>> grouped = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> grouped.groups = [1, 0, 1]
>>> group_order(grouped).groups
[0, 1, 1]

O último auxiliar remove termos em vez de reordená-los. Um termo que é um produto de operadores de número, bi†bib_i^\dagger b_i, apenas multiplica cada estado de Fock por uma fase, então filter_diagonal_terms() pode descartar a constante, os operadores de número e seus produtos quando essa fase é irrelevante para o que você calcula. Ele altera o operador in-place e exige um operador em ordem normal, o que ele não verifica:

>>> from qiskit_bosons.operators.terms import filter_diagonal_terms
>>> model = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(0), cre(0), ann(0), ann(0)): 0.5,
... (cre(0), ann(0)): 0.2,
... (): 0.1,
... }
... )
>>> filter_diagonal_terms(model)
>>> len(model)
2
>>> model.is_hermitian()
True

Restam apenas os dois termos de salto. A interação local e o operador de número são diagonais na base de ocupação, então são removidos junto com a constante. O modo repetido na interação é tratado corretamente: b0†b0†b0b0b_0^\dagger b_0^\dagger b_0 b_0 é diagonal, enquanto b0†b0†b0b_0^\dagger b_0^\dagger b_0 não seria.

Próximos passos​