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?

Transpilar um ansatz LUCJ

Importante

Os conceitos deste guia estão disponíveis apenas na API Python.

O ansatz de Jastrow de cluster unitário local (LUCJ) é uma parametrização compacta e eficiente em hardware de uma função de onda eletrônica correlacionada. Ele é membro da família mais geral de Jastrow de cluster unitário (UCJ) e tem a forma

∣Ψ⟩=(∏k=1LUk eiJk Uk†)∣Φ0⟩,\lvert \Psi \rangle = \left(\prod_{k=1}^{L} \mathcal{U}_k\, e^{i \mathcal{J}_k}\, \mathcal{U}_k^\dagger\right) \lvert \Phi_0 \rangle,

onde ∣Φ0⟩\lvert \Phi_0 \rangle é um estado de referência (tipicamente Hartree-Fock), cada Uk\mathcal{U}_k é uma rotação de orbitais, e cada Jk\mathcal{J}_k é um operador de Coulomb diagonal

J=12∑ij,στJijστ niσ njτ,\mathcal{J} = \frac12 \sum_{ij,\sigma\tau} \mathbf{J}^{\sigma\tau}_{ij}\, n_{i\sigma}\, n_{j\tau},

com niσn_{i\sigma} o operador de número no orbital espacial ii com spin σ\sigma.

O ffsim constrói o operador do ansatz; este pacote o transforma em um circuito e rebaixa esse circuito para qubits. Este guia trata da segunda metade dessa frase. O motivo para fazer esse caminho é o próprio rebaixamento: as portas Qiskit do próprio ffsim são específicas da transformação de Jordan-Wigner, enquanto um FermionicCircuit não carrega nenhuma suposição sobre sua codificação férmion-para-qubit, de modo que o mesmo ansatz pode ser sintetizado por qualquer codificação que sirva ao dispositivo de destino.

O operador do ansatz, portanto, chega aqui pronto, e tudo o que vem abaixo é o que acontece com ele depois: em que a porta se decompõe, como ela é rebaixada por Jordan-Wigner, como é ajustada ao mapa de acoplamento de um dispositivo e o que um rebaixamento diferente de Jordan-Wigner ainda exigiria. Para construir, parametrizar ou simular um ansatz UCJ em primeiro lugar, veja os guias do próprio ffsim.

Veja também

O guia de relação com o ffsim para saber como os dois pacotes dividem o trabalho, e o guia de transpilação para as etapas do pipeline usadas abaixo.

1. Obtenha um operador LUCJ do ffsim​

O ansatz é inicializado a partir das amplitudes de um cálculo de coupled-cluster com simples e duplas (CCSD). Execute Hartree-Fock restrito seguido de CCSD para uma molécula de hidrogênio na base 6-31g, usando o PySCF para a química quântica, e então passe as amplitudes para o from_t_amplitudes() do ffsim. Ele realiza uma fatoração dupla das amplitudes t2t_2 para obter as matrizes de Coulomb diagonais e as rotações de orbitais de cada camada, e deriva uma rotação de orbitais final opcional a partir das amplitudes t1t_1. O número de repetições LL é o que essa fatoração produzir; n_reps o trunca, trocando precisão por um circuito mais raso.

>>> import ffsim
>>> import pyscf
>>> import pyscf.cc
>>>
>>> # build the molecule and run Hartree-Fock
>>> mol = pyscf.gto.Mole()
>>> mol.build(
... atom=[["H", (0, 0, 0)], ["H", (0, 0, 0.74)]],
... basis="6-31g",
... symmetry="Dooh",
... verbose=0,
... )
<pyscf.gto.mole.Mole object at ...>
>>> scf = pyscf.scf.RHF(mol).run()
>>>
>>> norb = scf.mo_coeff.shape[1]
>>> nelec = (mol.nelec[0], mol.nelec[1])
>>>
>>> # run CCSD for the t-amplitudes, then factorize them into an ansatz operator
>>> ccsd = pyscf.cc.CCSD(scf).run()
>>> t1, t2 = ccsd.t1, ccsd.t2
>>>
>>> ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(t2, t1=t1, n_reps=2)

É aqui que o ansatz deixa de ser uma preocupação do ffsim. O ffsim oferece escolhas que este pacote não vê nem precisa conhecer, como a fatoração otimizada de forma variacional (“comprimida”) por trás de optimize=True, ou o empacotamento do vetor de parâmetros que um otimizador variacional manipula. Todas elas produzem o mesmo tipo de operador, e tudo o que vem abaixo funciona sem mudanças com qualquer uma delas.

2. Transforme o operador em um circuito fermiônico​

A porta UCJ envolve o operador e o expressa como um circuito sobre modos fermiônicos. A porta é um unitário puro, sem referência própria, então anexe no início uma porta InitializeModes (construída com from_hartree_fock()) para fornecer a referência de Hartree-Fock à qual o ansatz é aplicado.

>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import InitializeModes, UCJ
>>>
>>> ansatz = UCJ(ucj_op)
>>>
>>> circuit = FermionicCircuit(2 * norb)
>>> circuit.append(InitializeModes.from_hartree_fock(norb, nelec), circuit.modes)
>>> circuit.append(ansatz, circuit.modes)

Decompor o circuito revela sua anatomia, e essa é a representação sobre a qual o restante do guia opera. A porta InitializeModes prepara o determinante de referência, e cada camada do ansatz contribui com uma OrbitalRotation Uk†\mathcal{U}_k^\dagger, depois eiJke^{i\mathcal{J}_k} (uma Evolution do operador de Coulomb diagonal Jk\mathcal{J}_k), depois Uk\mathcal{U}_k, com uma OrbitalRotation final no fim. As rotações de orbitais atuam por setor de spin, de modo que cada uma é colocada de forma independente nos modos alfa 0..norb e nos modos beta norb..2*norb.

>>> circuit.decompose().draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The gates that the UCJ ansatz decomposes into.

Toda porta dessa decomposição ainda é fermiônica: OrbitalRotation e Evolution são definidas sobre modos, e nenhum operador de qubit ou de Pauli apareceu ainda. É isso que deixa a codificação em aberto, e é o estado em que o ansatz permanece até que a etapa de síntese escolha uma.

Nota

Cada camada termina com Uk\mathcal{U}_k e a seguinte começa com Uk+1†\mathcal{U}_{k+1}^\dagger, de modo que portas OrbitalRotation adjacentes poderiam ser mescladas em uma única rotação. Uma passagem de transpilação que realize essa fusão é um desenvolvimento futuro planejado.

3. Escolha uma codificação férmion-para-qubit​

O circuito ainda é fermiônico, e esse é o ponto em que este pacote mostra seu valor no fluxo de trabalho. Um FermionicCircuit não carrega nenhuma suposição sobre como os modos se tornam qubits, então a codificação é uma escolha da etapa de síntese, e não algo embutido no ansatz. Isso é o que este pacote acrescenta em relação às portas Qiskit do próprio ffsim, que são específicas da transformação de Jordan-Wigner.

Só para Jordan-Wigner você não precisaria desse desvio: o ffsim traz UCJOpSpinBalancedJW e suas variantes, que levam um operador UCJ a um circuito de qubits por conta própria. O motivo para passar o ansatz por um circuito fermiônico é tudo o mais que uma codificação pode oferecer. Codificações locais, por exemplo, gastam qubits extras para limitar o peso de Pauli de cada termo, o que os guias de conjuntos de fluxo 1D e 2D usam para tornar a profundidade de duas qubits de um passo de Trotter independente do tamanho do sistema.

Para um ansatz UCJ em particular, a etapa de síntese precisa cobrir dois tipos de porta:

  • A evolução de Coulomb diagonal. Evolution já é agnóstica à codificação: MapperFnEvolutionSynthesis recebe uma função de mapeamento, então as camadas eiJke^{i \mathcal{J}_k} são rebaixadas por qualquer codificação que você consiga expressar como uma. Os guias de conjuntos de fluxo mostram como escrever uma.

  • As rotações de orbitais e a preparação do determinante de Slater. Nem uma OrbitalRotation nem a PrepareSlaterDeterminant em que a InitializeModes inicial e a rotação se fundem (veja abaixo) é mapeada para qubits como tal. A passagem F2FSynthesis primeiro as decompõe, no espaço fermiônico, em portas Evolution de um ou dois modos cada (e, no caso da preparação, uma InitializeModes da referência), de modo que qualquer codificação capaz de mapear uma Evolution lida com elas. Sob Jordan-Wigner, os fatores entre modos adjacentes se tornam objetos XXPlusYYGatena etapa de qubits.

  • A inicialização. O plugin de InitializeModes não é parametrizado por uma codificação: ele documenta sua suposição de uma codificação na base de ocupação com mapeamento 1 para 1 entre modos e qubits, e emite portas diretamente nessa base. Uma codificação diferente precisa de seu próprio plugin, e nenhum é fornecido ainda.

Como transpilar circuitos UCJ sob uma codificação diferente de Jordan-Wigner é uma questão de pesquisa em aberto, e este pacote fornece o arcabouço no qual desenvolver tal pipeline. O restante deste guia constrói, como exemplo, o pipeline do transpilador para a codificação de Jordan-Wigner, equivalente ao produzido por generate_preset_jw_pass_manager().

4. Rebaixe-o por Jordan-Wigner​

O rebaixamento acontece em F2QSynthesis, que funciona como o HighLevelSynthesis do Qiskit: ele percorre as instruções fermiônicas e entrega cada uma a um plugin registrado para o nome dessa porta. Seu atributo F2QSynthesis.methods contém um F2QSynthesisPlugin por nome de porta, então montar a etapa significa escolher um plugin para cada porta fermiônica que o circuito contém:

>>> from qiskit_fermions.mappers.library import jordan_wigner
>>> from qiskit_fermions.transpiler.passes import F2QSynthesis
>>> from qiskit_fermions.transpiler.passes.synthesis import (
... MapperFnEvolutionSynthesis,
... TrivialOccupationInitializeModesSynthesis,
... )
>>>
>>> synthesis = F2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(jordan_wigner)
>>> synthesis.methods["InitializeModes"] = TrivialOccupationInitializeModesSynthesis()

Atribuir instâncias assim também é como um plugin que você mesmo escreveu entra no pipeline, sem precisar registrá-lo em lugar nenhum antes.

É aqui que a codificação é definida, nas duas formas descritas na seção 3: explicitamente, passando jordan_wigner() para MapperFnEvolutionSynthesis, e implicitamente, pelo outro plugin que já a assume.

Essa etapa então se encaixa em um MultiStagePassManager cujas etapas são executadas em uma ordem fixa:

>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit.transpiler import generate_preset_pass_manager
>>> from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig
>>>
>>> from qiskit_fermions.transpiler import (
... FermionicCircuitToDAG,
... FermionicPassManager,
... QuantumDAGToCircuit,
... )
>>> from qiskit_fermions.transpiler.passes import (
... F2FSynthesis,
... HopEvolutionSynthesis,
... MergeOrbitalRotations,
... MergeSlaterDeterminantPreparation,
... TrivialF2QLayout,
... )
>>>
>>> pm = MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... optimization=FermionicPassManager(
... [
... MergeOrbitalRotations(),
... MergeSlaterDeterminantPreparation(),
... F2FSynthesis(
... {"OrbitalRotation": "Givens", "PrepareSlaterDeterminant": "Givens"}
... ),
... ]
... ),
... layout=FermionicPassManager(TrivialF2QLayout()),
... synthesis=synthesis,
... qubit=generate_preset_pass_manager(
... hls_config=HLSConfig(PauliEvolution=[HopEvolutionSynthesis(), "default"])
... ),
... output=QuantumDAGToCircuit(),
... )

A etapa de otimização é executada antes da síntese, sobre o circuito fermiônico, e é isso que a torna valiosa: ela reescreve portas enquanto ainda são fermiônicas e agnósticas à codificação, de modo que a economia se estende a qualquer codificação que venha depois. MergeOrbitalRotations reduz cada sequência de portas OrbitalRotation consecutivas a uma só, o que importa para um ansatz UCJ porque toda camada termina com Uk\mathcal{U}_k e a seguinte começa com Uk+1†\mathcal{U}_{k+1}^\dagger. MergeSlaterDeterminantPreparation então funde a InitializeModes inicial e a rotação que a segue em uma única PrepareSlaterDeterminant, que a síntese rebaixa pela decomposição de Slater reduzida, mais barata, em vez de uma rotação quadrada completa. A ordem importa: mesclar as rotações primeiro expõe a rotação única imediatamente após a inicialização, que a fusão de Slater então absorve. F2FSynthesis vem por último, porque decompõe qualquer rotação que sobrar, e fazê-lo antes deixaria as duas passagens de fusão sem nada para fundir.

As demais etapas são infraestrutura. input e output convertem entre circuitos e DAGs, layout atribui modos fermiônicos a qubits (TrivialF2QLayout mapeia o modo i para o qubit i), e qubit é um pass manager comum do Qiskit que assume quando não resta nada fermiônico.

Executá-lo exige um passo preparatório: a porta composta UCJ precisa ser decomposta antes em suas portas primitivas, para que a etapa de otimização enxergue as rotações e evoluções individuais.

>>> transpiled = pm.run(circuit.decompose())
>>> print(dict(sorted(transpiled.count_ops().items())))
{'rz': 14, 'rzz': 12, 'x': 2, 'xx_plus_yy': 28}

Sem um dispositivo de destino, isso é mapeado em 2 * norb qubits assumindo conectividade todos-com-todos; as rotações de orbitais são sintetizadas em objetos XXPlusYYGatee as evoluções de Coulomb diagonais em objetos RZZGate:

>>> transpiled.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The Jordan-Wigner transpiled LUCJ circuit.

Esse pipeline é exatamente o que generate_preset_jw_pass_manager() monta, então na prática use o preset e construa as etapas manualmente quando precisar alterar alguma delas:

>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> preset = generate_preset_jw_pass_manager()
>>> preset.run(circuit.decompose()).count_ops() == transpiled.count_ops()
True

5. Ajuste à topologia do dispositivo​

Um dispositivo real tem um mapa de acoplamento de qubits restrito, e o ansatz LUCJ foi projetado para se ajustar a ele. As interações de mesmo spin (pairs_aa) formam duas cadeias lineares e as interações alfa-beta (pairs_ab) fazem a ponte entre elas. O generate_lucj_pass_manager() do ffsim constrói um pipeline de qubits ciente do dispositivo para essa estrutura, e retorna o subconjunto de pairs_ab que o hardware consegue de fato acomodar. Encaixe esse pipeline na etapa qubit do preset, mantendo a síntese férmion-para-qubit própria do pacote:

>>> from ffsim.qiskit import generate_lucj_pass_manager
>>> from qiskit.providers.fake_provider import GenericBackendV2
>>> from qiskit.transpiler import CouplingMap
>>>
>>> # a heavy-hex device coupling map (any BackendV2 works, e.g. a real fake_provider backend)
>>> coupling_map = CouplingMap.from_heavy_hex(5)
>>> backend = GenericBackendV2(
... num_qubits=coupling_map.size(),
... basis_gates=["cp", "xx_plus_yy", "p", "x", "swap"],
... coupling_map=coupling_map,
... )
>>>
>>> # nearest-neighbor same-spin chain; let the pass manager choose the alpha-beta pairs
>>> pairs_aa = [(p, p + 1) for p in range(norb - 1)]
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> hls_config = HLSConfig(PauliEvolution=[HopEvolutionSynthesis(), "default"])
>>> pm.qubit, allowed_pairs_ab = generate_lucj_pass_manager(
... backend,
... norb,
... "heavy-hex",
... (pairs_aa, None),
... optimization_level=3,
... seed_transpiler=0,
... hls_config=hls_config,
... )
>>>
>>> # the alpha-beta interactions the heavy-hex connectivity can implement
>>> print(allowed_pairs_ab)
[(0, 0)]

Essa composição é o ponto central: a etapa qubit é ciente do dispositivo, enquanto a etapa de síntese férmion-para-qubit que vem antes continua sendo a do próprio pacote e, portanto, continua substituível. Substituir a etapa qubit descarta o que o preset registra ali, que é o HopEvolutionSynthesis que transforma os fatores de Givens de uma rotação de orbitais em objetos XXPlusYYGatePasse-o adiante pelo hls_config como acima, ou a base do dispositivo precisará ser alcançada sem ele.

Com pm.qubit agora definido como o pipeline ciente do dispositivo, executar o pass manager posiciona o circuito nos qubits do backend e o roteia para o mapa de acoplamento. Para o ansatz irrestrito do passo 1, cujo operador de Coulomb diagonal ainda contém termos alfa-beta que o hardware não consegue alcançar diretamente, o roteador precisa inserir muitas portas SWAP para fazer a ponte:

>>> naive = pm.run(circuit.decompose())
>>> naive.num_qubits # laid out on the full heavy-hex device register
57
>>> naive_swaps = naive.count_ops()["swap"]
>>> naive_swaps # many SWAPs to bridge the unreachable alpha-beta interactions
33

Restrinja o ansatz às interações implementáveis no hardware

A solução é realimentar allowed_pairs_ab na construção do ansatz, por meio do argumento interaction_pairs de from_t_amplitudes(), de modo que o operador de Coulomb diagonal contenha apenas termos alfa-beta que o mapa de acoplamento consegue implementar diretamente. O ansatz então se ajusta à topologia do dispositivo e o roteador quase não precisa mexer nele:

>>> restricted = UCJ(
... ffsim.UCJOpSpinBalanced.from_t_amplitudes(
... t2, t1=t1, n_reps=2, interaction_pairs=(pairs_aa, allowed_pairs_ab)
... )
... )
>>>
>>> circuit = FermionicCircuit(2 * norb)
>>> circuit.append(InitializeModes.from_hartree_fock(norb, nelec), circuit.modes)
>>> circuit.append(restricted, circuit.modes)
>>>
>>> transpiled = pm.run(circuit.decompose())
>>> restricted_swaps = transpiled.count_ops()["swap"]
>>> restricted_swaps # far fewer routing SWAPs than the unrestricted ansatz
4

Desenhar apenas os qubits ativos (idle_wires=False) mostra o circuito restrito às duas cadeias de spin e à ponte alfa-beta, expresso nas portas de base do dispositivo. O layout e o roteamento espalham os modos lógicos pelos qubits físicos do dispositivo, então passe um wire_order tirado do layout final do circuito (final_index_layout() lista o qubit físico em que cada qubit de entrada terminou, na ordem dos qubits de entrada) para desenhar os fios de volta na ordem original dos modos:

>>> wire_order = transpiled.layout.final_index_layout(filter_ancillas=False)
>>> transpiled.draw("mpl", idle_wires=False, fold=-1, wire_order=wire_order)
<Figure size ... with 1 Axes>

The hardware-restricted LUCJ circuit routed onto the heavy-hex device coupling map.

Nota

As contagens exatas de portas e a profundidade após o layout dependem das passagens de roteamento/otimização e do dispositivo escolhido, por isso não são reproduzidas aqui. O ponto principal é o co-design; expressar o ansatz com uma cadeia pairs_aa de vizinhos mais próximos e pontes pairs_ab filtradas pelo hardware mantém o circuito sintetizado próximo da topologia do dispositivo, minimizando a sobrecarga de roteamento (portas SWAP inseridas). Veja FermionicSlaterGivensDecomposition para uma escolha de síntese relacionada (minimize_2q_gate_count), que troca a contagem de portas de dois qubits pela profundidade roteada.

Próximos passos​

  • Saiba como as portas individuais funcionam na documentação de qiskit_fermions.circuit.library e no guia de circuito fermiônico.

  • Explore o guia de explicação de operadores para entender como construir operadores fermiônicos e Hamiltonianos diretamente.

  • Veja como um circuito fermiônico é mapeado para qubits no guia de transpilação.

  • Leia a documentação de F2QSynthesis para conhecer a outra forma de configurar a etapa de síntese: um dicionário config que seleciona plugins pelos nomes curtos com que eles se registram em seus entry points qiskit_fermions.transpiler.synthesis, em vez de por instância.

  • Leia o guia de relação com o ffsim para entender como os dois pacotes dividem o trabalho e quais protocolos permitem que o ffsim simule os circuitos deste pacote.