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?

Sintetize uma porta Evolution no espaço fermiônico

Importante

Os conceitos deste guia estão disponíveis atualmente apenas na API Python. Uma funcionalidade equivalente será disponibilizada na API C em uma versão futura.

Uma porta Evolution carrega a evolução temporal e−itHe^{-itH} de um operador inteiro. Uma síntese de férmion para férmion reescreve essa porta em um FermionicCircuit de portas fermiônicas menores. Ambos os lados da reescrita permanecem no espaço fermiônico, o que a distingue da síntese de férmion para Qubit que vem depois: os groups do operador (veja Agrupe termos do operador: use a estrutura do operador) sobrevivem a ela, enquanto um mapeamento de férmion para Qubit retorna um SparseObservable, que não tem conceito de agrupamento.

Dois controles selecionam o método. Evolution.synthesis o define em uma porta, e a passagem de transpilador F2FSynthesis o define em um circuito inteiro. Ambos recebem um F2FSynthesisPlugin, a interface que qualquer reescrita desse tipo implementa. Os métodos que este pacote fornece atualmente para uma evolução são fórmulas de produto, que é o que o restante deste guia usa: FermionicLieTrotter para uma fórmula de primeira ordem (o padrão) e FermionicSuzukiTrotter para uma ordem par mais alta. Qualquer outra coisa que implemente a interface é selecionada da mesma forma, e as seções sobre os dois controles se aplicam a ela sem alterações.

Nota

Esta etapa de férmion para férmion é opcional. Uma porta Evolution pode ir direto para a etapa de férmion para Qubit, que a mapeia e sintetiza inteira, não importa quantos termos ela contenha. Dividi-la antes é uma escolha, feita porque as portas resultantes são individualmente mais baratas ou porque a divisão expõe estrutura que uma etapa posterior pode usar.

Os dois passos se compõem, então uma evolução pode acabar aproximada em qualquer um dos níveis, em ambos ou em nenhum. Uma fórmula de produto é aproximada, a menos que seus fatores comutem; um método de síntese não precisa aproximar de forma alguma.

Nota

Pular esta etapa não precisa significar perder o agrupamento por completo. Envolver o mapeador em group_wise() mapeia o operador um grupo por vez e soma os resultados, de modo que os termos de Pauli mapeados chegam agrupados em vez de intercalados. Uma fórmula de produto do lado dos Qubits sintetiza os termos na ordem em que os recebe, então termos que comutam e acabam adjacentes podem ser agendados juntos.

No Hamiltoniano abaixo, isso recupera todo o benefício de profundidade de dois Qubits obtido com a divisão: 18 em vez de 30, com a mesma contagem de portas. A diferença está em onde a estrutura é expressa. Dividir no espaço fermiônico faz de cada grupo uma porta separada, que toda etapa posterior pode ver e sobre a qual pode agir; group_wise deixa uma única porta e apenas reordena os termos dentro dela, o que basta para o agendamento mas não deixa nada para uma passagem fermiônica trabalhar. Use-o quando a evolução precisar continuar sendo uma única porta e recorra a uma fórmula de produto fermiônica nos demais casos.

Construa um Hamiltoniano agrupado​

Ambas as fórmulas de produto dividem um operador em fatores e, para um operador agrupado, esses fatores são seus grupos. O agrupamento é, portanto, o que determina tanto o custo quanto a precisão do resultado. Tome uma cadeia de Fermi-Hubbard de seis sítios, agrupada da maneira como seus termos comutam: os termos de salto nas ligações pares, os termos de salto nas ligações ímpares e a interação diagonal.

>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> def fermi_hubbard_chain(num_sites, tunneling, interaction):
... """A 1D Fermi-Hubbard chain grouped into even bonds, odd bonds, and the interaction."""
... terms, groups = [], []
... for offset in (0, 1):
... for j in range(offset, num_sites - 1, 2):
... terms += [
... ([cre(j), ann(j + 1)], -tunneling),
... ([cre(j + 1), ann(j)], -tunneling),
... ]
... groups += [offset, offset]
... for j in range(num_sites - 1):
... terms.append(([cre(j), ann(j), cre(j + 1), ann(j + 1)], interaction))
... groups.append(2)
... operator = FermionOperator.from_terms(terms)
... operator.groups = groups
... return operator
>>>
>>> num_sites = 6
>>> hamiltonian = fermi_hubbard_chain(num_sites, tunneling=1.0, interaction=2.0)
>>> hamiltonian.num_groups()
3

Quinze termos em três grupos. Cada grupo de salto emparelha cada termo com seu parceiro conjugado, o que importa: um fator precisa ser hermitiano para que sua exponencial seja unitária, e isso não decorre de a soma ser hermitiana. groups_are_hermitian() verifica essa convenção em vez de deixá-la para inspeção visual:

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

Separar a0†a1a^\dagger_0 a_1 de seu parceiro a1†a0a^\dagger_1 a_0 quebraria isso, e deixar o operador sem agrupamento tem o mesmo efeito, já que ambas as fórmulas então o dividem termo a termo e um ai†aja^\dagger_i a_j isolado não é hermitiano:

>>> ungrouped = fermi_hubbard_chain(num_sites, tunneling=1.0, interaction=2.0)
>>> ungrouped.groups = range(len(list(ungrouped.iter_terms()))) # one group per term
>>> all(groups_are_hermitian(ungrouped))
False

Tal fator é um operador fermiônico válido, e este pacote construirá a porta para ele, então nada reclama até que o operador mapeado chegue a PauliEvolutionGate, que exige coeficientes reais e levanta ValueError: Operator contains complex coefficients, which are not supported. Verificar o agrupamento de antemão transforma essa falha tardia em uma resposta disponível antes de qualquer circuito ser construído.

Defina o método em uma porta​

Passe um método de síntese para a porta e depois decomponha-a. Cada porta que ela emite volta como sua própria porta Evolution:

>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.circuit.library.synthesis import (
... FermionicLieTrotter,
... FermionicSuzukiTrotter,
... )
>>>
>>> total_time = 1.0
>>>
>>> def factors(synthesis):
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(num_sites, hamiltonian, time=total_time, synthesis=synthesis),
... circuit.modes,
... )
... return circuit.decompose().count_ops()["Evolution"]
>>>
>>> factors(FermionicLieTrotter())
3
>>> factors(FermionicSuzukiTrotter(order=2))
5
>>> factors(FermionicSuzukiTrotter(order=2, reps=4))
17
>>> factors(FermionicSuzukiTrotter(order=2, reps=4, merge_adjacent=False))
20

A primeira ordem emite um fator por grupo. A ordem dois emite o palíndromo simétrico, de modo que os dois grupos externos aparecem duas vezes com metade do tempo cada, dando cinco. Quatro repetições disso dariam vinte, mas cada repetição termina no mesmo grupo com o qual a seguinte começa. Essas portas vizinhas evoluem sob o mesmo operador, então são fundidas em uma única porta com o tempo somado, o que economiza uma porta por junção. Passe merge_adjacent=False para manter a sequência de livro-texto. Toda ordem divide o operador da mesma maneira; o que muda é a ordenação dos fatores e seus tempos de evolução, não a partição.

Nota

Os fatores são marcados como atomic, então decompor novamente os deixa no lugar em vez de dividi-los ainda mais. A decomposição tem um ponto fixo, o que torna seguro chamar decompose() repetidamente.

Defina o método para um pipeline inteiro​

Definir synthesis por porta significa propagar a escolha por tudo que constrói uma, incluindo UCC e UCJ, que constroem suas portas internamente. A passagem F2FSynthesis aplica um método a toda Evolution em um circuito, então a escolha é feita uma única vez para o pipeline. Ela é configurada pelo nome da porta, da mesma forma que F2QSynthesis, e uma porta que não é nomeada é deixada como está:

>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.passes import F2FSynthesis
>>>
>>> circuit = FermionicCircuit(num_sites)
>>> circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
>>>
>>> expanded = FermionicPassManager(
... F2FSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2, "reps": 4})})
... ).run(circuit)
>>> expanded.count_ops()["Evolution"]
17

Esta é a mesma contagem que decompose() dá para esse método acima. A passagem MergeEvolutions aplica a mesma fusão a portas vizinhas que não vieram de uma única síntese, como as junções entre circuitos consecutivos. A passagem executa a fórmula por conta própria, então nenhuma etapa de expansão separada é necessária, e executá-la duas vezes não muda nada, já que os fatores que ela emite são atomic e, portanto, não carregam definição própria.

Nota

Passe apply=False para apenas selecionar o método e deixar a expansão para outra coisa, como o Decompose do Qiskit (nomear a porta como Decompose("Evolution") o restringe às evoluções, deixando as demais portas fermiônicas para seus próprios plugins de síntese).

Isso é útil para escolher o método de um circuito inteiro de antemão, adiando a expansão para o momento em que ela for desejada, por exemplo para inspecionar o circuito primeiro:

>>> selected = FermionicPassManager(
... F2FSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}, apply=False)
... ).run(circuit)
>>> selected.count_ops()["Evolution"] # selected, not yet expanded
1
>>> selected.decompose().count_ops()["Evolution"] # expanded on demand
5

Esteja ciente, porém, do que uma porta não expandida faz: ela chega inteira à etapa de férmion para Qubit, onde é mapeada sem que synthesis seja consultado. Isso facilita um erro ao comparar métodos, porque todos eles então produzem saída idêntica e a comparação silenciosamente não mede nada. No Hamiltoniano acima, deixar a porta de ordem dois sem expandir reduz o circuito muito maior abaixo ao custo de uma única evolução mapeada, não importa qual método tenha sido selecionado.

Meça o ganho​

Uma ordem mais alta custa profundidade, então a questão não é se ela é mais precisa, mas se é mais precisa por porta. São necessários dois números por método: o erro do estado que ele produz e o custo de dois Qubits do circuito que o produziu.

O erro é medido com o ffsim, que simula um FermionicCircuit diretamente (veja Como este pacote se relaciona com o ffsim). Nenhum mapeamento de férmion para Qubit está envolvido, então a comparação isola a fórmula de produto fermiônica de qualquer coisa que o mapeamento ou a síntese do lado dos Qubits possa contribuir. Ele também trabalha no setor de número fixo de partículas, que é menor que o espaço completo 2N2^N e é o espaço em que a evolução de fato permanece.

>>> import ffsim
>>> import numpy as np
>>> from scipy.sparse.linalg import expm_multiply
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> norb, nelec = num_sites, (3, 0) # three spinless fermions on six modes
>>> initial = ffsim.hartree_fock_state(norb, nelec)
>>>
>>> hamiltonian_operator = ffsim.linear_operator(hamiltonian, norb=norb, nelec=nelec)
>>> exact = expm_multiply(
... -1j * total_time * hamiltonian_operator,
... initial,
... traceA=-1j * total_time * ffsim.trace(hamiltonian, norb=norb, nelec=nelec),
... )
>>>
>>> def cost_and_error(synthesis):
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(num_sites, hamiltonian, time=total_time, synthesis=synthesis),
... circuit.modes,
... )
... # the method is set on the gate, so decompose() is what expands it here; a pipeline
... # would reach for F2FSynthesis instead (see the note above)
... circuit = circuit.decompose()
... evolved = ffsim.apply_unitary(initial, circuit, norb=norb, nelec=nelec)
... error = 1 - abs(np.vdot(evolved, exact)) ** 2
... qubit_circuit = generate_preset_jw_pass_manager().run(circuit).decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... return error, qubit_circuit.count_ops()["cx"], qubit_circuit.depth(two_qubit)
>>>
>>> methods = [
... ("order 1, reps 1 ", FermionicLieTrotter()),
... ("order 2, reps 1 ", FermionicSuzukiTrotter(order=2)),
... ("order 1, reps 10", FermionicLieTrotter(reps=10)),
... ("order 2, reps 4 ", FermionicSuzukiTrotter(order=2, reps=4)),
... ]
>>> costs = {label: cost_and_error(synthesis) for label, synthesis in methods}
>>> labels, counts, depths = [], [], []
>>> for label, (error, gates, depth) in costs.items():
... labels.append(label.strip().replace(", ", "\n"))
... counts.append(gates)
... depths.append(depth)
... print(f"{label} error {error:.6f}")
order 1, reps 1 error 0.515997
order 2, reps 1 error 0.180958
order 1, reps 10 error 0.004026
order 2, reps 4 error 0.000421

O custo de dois Qubits foi deixado de fora dessa tabela de propósito. Ele depende da síntese do lado dos Qubits que o Qiskit seleciona, a qual melhora entre versões, então os números impressos aqui seriam um instantâneo, e não uma propriedade da fórmula de produto. A figura abaixo os plota em vez disso, e aquilo em que a comparação se apoia é afirmado como uma relação:

Grouped bars of CX count and depth per method, with order two at four repetitions clearly shorter than order one at ten.

Leia os dois últimos métodos juntos, porque são a comparação que importa. A ordem dois com quatro repetições é aproximadamente dez vezes mais precisa que a ordem um com dez, e chega lá com menos portas emaranhadoras, e não mais, como mostra a figura. Gastar um orçamento de portas em uma ordem mais alta supera gastá-lo em mais passos de primeira ordem aqui.

Esse resultado não é universal. Aumentar reps e aumentar order reduzem o erro em taxas diferentes para o mesmo tipo de custo, e qual vence depende do operador e do tempo evoluído. As duas linhas acima são a maneira de descobrir isso para um dado problema.

Nota

A fórmula fermiônica se compõe com a escolhida para a etapa de férmion para Qubit (veja MapperFnEvolutionSynthesis.product_formula). Quando ambas aproximam, a precisão do resultado é governada pela mais fraca das duas, então aumentar a ordem aqui enquanto a fórmula do lado dos Qubits continua de primeira ordem rende pouco.

Restrinja a passagem às portas que se beneficiam​

Uma ordem mais alta só ajuda quando os fatores não comutam. Se todos comutam, a fórmula de primeira ordem já é exata e os fatores extras são puro excesso. Um operador puramente diagonal torna isso concreto, já que operadores de número comutam entre si:

>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> diagonal = FermionOperator.from_terms(
... [([cre(j), ann(j), cre(j + 1), ann(j + 1)], 1.0 + 0.1 * j) for j in range(num_sites - 1)]
... )
>>> diagonal.groups = list(range(num_sites - 1))
>>>
>>> def diagonal_cost(synthesis):
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(num_sites, diagonal, time=total_time, synthesis=synthesis), circuit.modes
... )
... transpiled = generate_preset_jw_pass_manager().run(circuit.decompose())
... return transpiled.decompose(reps=6).count_ops()["cx"]
>>>
>>> diagonal_cost(FermionicLieTrotter())
10
>>> diagonal_cost(FermionicSuzukiTrotter(order=2))
18

Ambos os circuitos são exatos, então o segundo gasta 80% mais portas emaranhadoras à toa. Os operadores de Coulomb diagonais de um UCJ têm esse formato, e é por isso que F2FSynthesis.filter existe: ele recebe um predicado sobre o DAGOpNode e deixa intocadas as portas rejeitadas, de modo que uma única passagem pode elevar a ordem nas portas que se beneficiam e deixar o restante na primeira ordem.

Um circuito contendo os dois tipos de evolução mostra o que isso economiza. O predicado abaixo aceita uma porta somente quando seu operador move partículas entre modos, que é exatamente quando os grupos não comutam e uma ordem mais alta tem algo a render:

>>> def is_diagonal(operator):
... """Whether every term only counts particles, rather than moving them between modes."""
... return all(
... sorted(mode for created, mode in actions if created)
... == sorted(mode for created, mode in actions if not created)
... for actions, _ in operator.iter_terms()
... )
>>>
>>> def mixed_circuit():
... circuit = FermionicCircuit(num_sites)
... circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
... circuit.append(Evolution(num_sites, diagonal, time=total_time), circuit.modes)
... return circuit
>>>
>>> def two_qubit_cost(circuit):
... transpiled = generate_preset_jw_pass_manager().run(circuit).decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... return transpiled.count_ops()["cx"], transpiled.depth(two_qubit)
>>>
>>> order_2 = FermionicSuzukiTrotter(order=2)
>>>
>>> everywhere = two_qubit_cost(
... FermionicPassManager(F2FSynthesis({"Evolution": order_2})).run(mixed_circuit())
... )
>>> restricted = two_qubit_cost(
... FermionicPassManager(
... F2FSynthesis(
... {"Evolution": order_2}, filter=lambda node: not is_diagonal(node.op.operator)
... )
... ).run(mixed_circuit())
... )
>>> everywhere[0] - restricted[0] # the gates the filter saved
8
>>> restricted[0] < everywhere[0] and restricted[1] < everywhere[1]
True

As portas que o filtro economiza são exatamente as que a ordem dois gastou na evolução diagonal, que a comparação diagonal acima avalia em oito (18 contra 10). Elas não renderam nada ali, porque essa evolução já é exata na primeira ordem, então descartá-las não custa precisão.

Nota

Compare os dois circuitos de ordem dois entre si, e não com o não expandido. Deixar ambas as portas sem expandir envia cada uma inteira à etapa de férmion para Qubit, que é o erro que a nota sobre apply=False acima descreve: ele pode cair na mesma contagem de portas emaranhadoras que o circuito filtrado sendo, ao mesmo tempo, um circuito diferente com profundidade diferente, então ler o benefício do filtro a partir desse número não mede nada.

Cuidado

Nenhum dos controles verifica se os fatores são hermitianos, e um operador agrupado incorretamente pode parecer melhor em uma ordem par do que na primeira ordem e ainda assim estar errado, porque a simetrização cancela parcialmente o erro. Agrupe parceiros conjugados juntos, como fermi_hubbard_chain acima faz.

Próximos passos​