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?

Agrupar termos de operadores: use a estrutura do operador

Um BosonOperator pode marcar cada um de seus termos com um índice de grupo. As marcações não têm significado próprio: elas apenas dizem quais termos andam juntos. O que as marcações significam é decidido por quem as consome. Neste pacote, os consumidores são a porta de evolução temporal, que é dividida em um fator por grupo, e o passo aleatório qDRIFT, que amostra grupos inteiros.

Este guia mostra como atribuir grupos, como verificar as propriedades em que um consumidor se apoia e como organizar o operador para que escolher alguns grupos dentro de um grande continue barato.

Nota

Nenhuma estratégia de agrupamento automático acompanha este pacote. Você atribui os índices por conta própria, a partir do que sabe sobre seu modelo.

Atribua grupos​

O exemplo ao longo do guia é uma cadeia de Bose-Hubbard de três sítios. Cada ligação contribui com um par de salto, e as interações no sítio formam uma parte diagonal. A forma mais simples de construir um operador agrupado é from_terms_with_groups(), que recebe cada termo junto com seu índice de grupo como uma tripla (actions, coefficient, group). Você decide o grupo no momento em que escreve o termo, quando ainda sabe por que ele pertence ali:

>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> bonds = [(0, 1), (1, 2)]
>>> terms = []
>>> for group, (left, right) in enumerate(bonds): # one group per bond
... terms.append(([cre(left), ann(right)], -1.0, group))
... terms.append(([cre(right), ann(left)], -1.0, group))
>>> for site in range(3): # the on-site interactions share one group
... terms.append(([cre(site), cre(site), ann(site), ann(site)], 1.0, len(bonds)))
>>> hamiltonian = BosonOperator.from_terms_with_groups(terms)
>>> len(hamiltonian), hamiltonian.num_groups()
(7, 3)

Os termos são armazenados na ordem em que você os fornece, então os índices de grupo não podem acabar associados aos termos errados.

Um operador que já existe precisa ser agrupado depois, atribuindo o atributo groups. É aí que se precisa de cuidado, porque from_dict() não especifica a ordem em que armazena seus termos. Escrever a lista por posição pode, então, associar um índice ao termo errado. Em vez disso, derive cada índice do próprio termo:

>>> existing = BosonOperator.from_dict({tuple(actions): coeff for actions, coeff, _ in terms})
>>> existing.groups = [
... len(bonds)
... if len({mode for _, mode in term}) == 1
... else bonds.index(tuple(sorted({mode for _, mode in term})))
... for term, _ in existing.iter_terms()
... ]
>>> existing.num_groups(), existing.equiv(hamiltonian)
(3, True)

O número de índices precisa corresponder ao número de termos. Qualquer outra coisa é rejeitada, porque uma lista curta demais descartaria silenciosamente os termos finais onde quer que os termos sejam iterados junto com seus grupos:

>>> try:
... hamiltonian.groups = [0, 1]
... except ValueError as error:
... print(error)
expected one group index per term, but got 2 for 7 terms

Verifique em que o consumidor se apoia​

Três funções em qiskit_bosons.operators.terms respondem, cada uma, a uma pergunta específica sobre um agrupamento existente. Nenhuma delas diz que um agrupamento é “correto”, porque a correção é relativa ao consumidor.

Cada grupo é hermitiano? Esta é a propriedade que mais importa. A evolução temporal de um grupo só é unitária se o grupo for hermitiano, e um termo de salto isolado bi†bjb^\dagger_i b_j não é. Somente o par com seu adjunto é hermitiano:

>>> from qiskit_bosons.operators.terms import groups_are_hermitian
>>> groups_are_hermitian(hamiltonian)
[True, True, True]

Um agrupamento que separa um par de salto é detectado imediatamente:

>>> split = BosonOperator.from_terms_with_groups(
... [(actions, coeff, index) for index, (actions, coeff, _) in enumerate(terms)]
... ) # one term per group
>>> [bool(flag) for flag in groups_are_hermitian(split)]
[False, False, False, False, True, True, True]

Os quatro termos de salto (hopping) são detectados. As três entradas True são os termos locais (on-site) bi†bi†bibib^\dagger_i b^\dagger_i b_i b_i, que são hermitianos por si só. Um modo pode se repetir legitimamente dentro de um termo, e nada é descartado por isso. As flags seguem a ordem de terms, e é por isso que cada uma pode ser lida em relação ao termo que ela descreve.

Os coeficientes são uniformes? Uma fórmula de produto aleatória pondera um grupo pela magnitude média de seus coeficientes, o que só é uma descrição fiel do grupo se as magnitudes coincidirem:

>>> from qiskit_bosons.operators.terms import group_coeff_means, groups_have_uniform_coeffs
>>> groups_have_uniform_coeffs(hamiltonian)
[True, True, True]
>>> group_coeff_means(hamiltonian)
[1.0, 1.0, 1.0]

Nenhuma das duas verificações implica a outra: coeficientes uniformes não tornam um grupo hermitiano, e um grupo hermitiano pode misturar magnitudes. Por padrão, as magnitudes são comparadas, porque é isso que group_coeff_means() calcula a média. Um par hermitiano com coeficientes complexos tem magnitudes iguais, mas coeficientes diferentes, então satisfaz apenas a forma padrão:

>>> conjugate = BosonOperator.from_terms_with_groups(
... [([cre(0), ann(1)], 1.0j, 0), ([cre(1), ann(0)], -1.0j, 0)]
... )
>>> groups_have_uniform_coeffs(conjugate)
[True]
>>> groups_have_uniform_coeffs(conjugate, abs=False)
[False]
Nota

groups_are_hermitian() é construída sobre is_hermitian(), que compara um operador com seu adjunto após a ordenação normal. Essa é uma identidade da álgebra não truncada, então um grupo que passa é hermitiano como operador bosônico. Veja Princípios de design das representações de operadores para saber como os produtos de operadores de escada são ordenados e comparados.

Ordene os termos por grupo​

split_out_groups() retorna um operador para cada grupo solicitado. Quando os termos de um grupo estão espalhados pelo operador, ela precisa percorrer todos os termos para encontrá-los. Quando cada grupo forma uma sequência contígua, ela faz uma busca binária pelos limites do grupo, então o custo de uma consulta acompanha o número de grupos solicitados, e não o número de termos armazenados.

group_order() produz esse layout. É uma ordenação estável pelo índice do grupo: o valor do operador não muda, e os termos dentro de um grupo mantêm sua ordem relativa.

>>> from qiskit_bosons.operators.terms import group_order
>>> scattered = BosonOperator.from_terms_with_groups(
... [(actions, coeff, group) for (actions, coeff, _), group in zip(terms, [1, 0, 1, 0, 2, 2, 2])]
... )
>>> ordered = group_order(scattered)
>>> ordered.groups
[0, 0, 1, 1, 2, 2, 2]
>>> ordered.equiv(scattered)
True

Os dois layouts retornam os mesmos operadores; só o custo muda:

>>> request = [2, 0]
>>> all(
... a.equiv(b)
... for a, b in zip(
... scattered.split_out_groups(request), ordered.split_out_groups(request), strict=True
... )
... )
True

Próximos passos​

O agrupamento é escolhido aqui, no nível bosônico, enquanto a estrutura do modelo ainda está visível.