Otimize a preparação de determinantes de Slater
Os conceitos deste guia estão atualmente disponíveis apenas na API Python. Funcionalidade equivalente será disponibilizada na API C em uma versão futura.
Uma porta InitializeModes declara uma ocupação de modos de referência; uma porta OrbitalRotation gira a base de uma partícula. Colocadas em sequência, as duas portas preparam um determinante de Slater; os modos começam no determinante de referência e são então girados para os orbitais de destino. A porta PrepareSlaterDeterminant captura essa composição e, como carrega a ocupação de referência, transpilá-la pode usar a givens_decomposition_slater() retangular, que realiza apenas os orbitais ocupados, em vez da decomposição quadrada completa que uma OrbitalRotation sozinha exige. Essa é uma síntese estritamente mais barata (veja o ganho no final deste guia).
O passe de transpilador MergeSlaterDeterminantPreparation detecta o padrão InitializeModes seguido de OrbitalRotation em um FermionicCircuit e o reescreve em portas PrepareSlaterDeterminant, de modo que uma etapa de síntese posterior aproveita automaticamente a decomposição reduzida. Em simulação, a reescrita não altera o estado: PrepareSlaterDeterminant é validar e depois girar (ela valida a ocupação de referência e então aplica a rotação, exatamente como as duas portas separadas), então a fusão habilita a síntese mais barata sem alterar o estado preparado.
Ao longo deste guia, cada circuito é desenhado antes e depois do passe, lado a lado: a entrada à esquerda e o resultado da execução do passe sobre ela à direita. Dois auxiliares fazem isso. merge executa o passe em uma cópia do circuito (por meio de seu FermionicDAGCircuit) e converte o resultado de volta em um FermionicCircuit desenhável, e draw_merge desenha as duas metades em uma única figura de antes e depois:
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>>
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import InitializeModes, OrbitalRotation
>>> from qiskit_fermions.transpiler import FermionicCircuitToDAG
>>> from qiskit_fermions.transpiler.converters import FermionicDAGToCircuit
>>> from qiskit_fermions.transpiler.passes import MergeSlaterDeterminantPreparation
>>>
>>> def merge(circuit):
... dag = FermionicCircuitToDAG().run(circuit)
... merged = MergeSlaterDeterminantPreparation().run(dag)
... return FermionicDAGToCircuit().run(merged)
>>>
>>> def natural_size(circuit):
... # the figure size Qiskit picks for a circuit on its own (discard the probe figure)
... probe = circuit.draw("mpl")
... size = probe.get_size_inches()
... plt.close(probe)
... return size
>>>
>>> def draw_merge(circuit):
... merged = merge(circuit)
... # let Qiskit size each half naturally, then lay them out side by side
... (bw, bh), (aw, ah) = natural_size(circuit), natural_size(merged)
... fig, (before, after) = plt.subplots(1, 2, figsize=(bw + aw, max(bh, ah)))
... circuit.draw("mpl", ax=before)
... merged.draw("mpl", ax=after)
... before.set_title("before")
... after.set_title("after")
... return fig
O que é fundido
O passe reconhece três padrões, todos baseados na convenção de modos de spin em blocos que InitializeModes e OrbitalRotation usam. Para um sistema com norb orbitais espaciais, os modos 0..norb formam o setor de spin alfa e norb..2*norb formam o setor de spin beta. A preparação do determinante de Slater é feita por setor de spin, porque cada setor tem seu próprio formato (elétrons, orbitais).
Registrador completo (ou sem spin)
O padrão mais simples é usar InitializeModes, imediatamente seguido de uma OrbitalRotation nos mesmos modos. Este é o caso sem spin (um determinante sobre todos os modos) e também o de um circuito com spin cuja rotação abrange ambos os setores de uma vez. As duas portas se fundem em uma única PrepareSlaterDeterminant:
>>> circuit = FermionicCircuit(4)
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(4)), circuit.modes)
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>
Por setor de spin
Este é o mesmo formato, mas restrito a uma metade de spin. Aqui, dois pares independentes de inicialização seguida de rotação, um por setor, se fundem cada um por conta própria em uma PrepareSlaterDeterminant:
>>> circuit = FermionicCircuit(6) # norb = 3: alpha modes 0..3, beta modes 3..6
>>> circuit.append(InitializeModes([1, 1, 0]), circuit.modes[:3])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> circuit.append(InitializeModes([1, 0, 0]), circuit.modes[3:])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[3:])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>
Inicialização global, rotações por spin
Isso envolve uma única InitializeModes de registrador completo (2 * norb) seguida de duas ações OrbitalRotation, uma em cada metade de spin contígua. O passe divide a ocupação global no limite do setor e emite duas portas PrepareSlaterDeterminant. As duas rotações podem aparecer em qualquer ordem.
Este é o formato produzido pelo fluxo de trabalho do cluster unitário local Jastrow (LUCJ) (consulte o guia de LUCJ); o usuário coloca uma InitializeModes (normalmente a referência de Hartree-Fock, usando from_hartree_fock()) no início do circuito e acrescenta um ansatz UCJ, cujas duas primeiras rotações orbitais por spin seguem diretamente a inicialização depois que o ansatz é decomposto.
>>> circuit = FermionicCircuit(6) # norb = 3, nelec = (2, 1)
>>> circuit.append(InitializeModes.from_hartree_fock(3, (2, 1)), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[3:])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>
Esse padrão também dispara quando apenas uma metade de spin é girada por uma porta que segue diretamente a inicialização, o formato produzido quando a rotação orbital de apenas um setor de spin fica no início do circuito. A metade girada se torna uma PrepareSlaterDeterminant com sua rotação, e a outra metade é preparada com uma rotação identidade. A preparação identidade é sintetizada nas portas X de referência que a InitializeModes teria emitido de qualquer forma para essa metade, então completá-la não custa portas extras e ainda habilita a síntese reduzida de Slater na metade girada:
>>> circuit = FermionicCircuit(6)
>>> circuit.append(InitializeModes.from_hartree_fock(3, (2, 1)), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>
O que permanece intocado
O passe é deliberadamente conservador. Ele só funde quando as duas portas realmente compõem uma preparação de determinante de Slater, e copia todo o resto sem alterações. “Imediatamente seguida” é entendido sobre o grafo de fluxo de dados do circuito. A OrbitalRotation só é fundida quando a InitializeModes é seu único predecessor em todos os seus modos. Nas figuras abaixo, o antes e o depois são idênticos: nada foi fundido.
Uma operação no meio
Se qualquer porta tocar os modos entre a inicialização e a rotação, elas deixam de compor uma preparação pura, então nada é fundido:
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.operators import FermionOperator, ann, cre
>>>
>>> circuit = FermionicCircuit(4)
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> number_op = FermionOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> circuit.append(Evolution(4, number_op, time=0.5), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(4)), circuit.modes)
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>
Uma rotação parcial que não é uma metade de spin
Uma rotação que cobre exatamente uma metade de spin contígua de uma inicialização de registrador completo é fundida (veja a seção anterior). Mas uma rotação sobre qualquer outro conjunto parcial de modos (menos modos que a inicialização e que não seja uma de suas duas metades de spin, como um subintervalo que atravessa o limite do setor) não é uma preparação por setor. Fundi-la descartaria silenciosamente o papel da inicialização nos modos que ela deixa de fora, então o passe mantém as duas portas no lugar:
>>> circuit = FermionicCircuit(4) # norb = 2: the only spin halves are modes [0, 1] and [2, 3]
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>
Por que isso importa: o ganho da síntese
A fusão compensa porque PrepareSlaterDeterminant é sintetizada em muito menos portas que a OrbitalRotation que ela absorve. Um determinante de elétrons sobre orbitais precisa de no máximo rotações de dois qubits e nenhuma porta de fase diagonal, contra as rotações mais fases da rotação orbital quadrada completa.
O pass manager predefinido de Jordan-Wigner executa MergeSlaterDeterminantPreparation em sua etapa de otimização, então essa redução acontece automaticamente. Transpile uma preparação de dois elétrons e seis orbitais até chegar a um QuantumCircuit e conte suas portas:
O preset executa MergeOrbitalRotations imediatamente antes de MergeSlaterDeterminantPreparation. Isso importa quando uma sequência de portas OrbitalRotation consecutivas segue a inicialização. O passe anterior primeiro colapsa a sequência em uma única rotação, que MergeSlaterDeterminantPreparation então contrai com a inicialização em uma única PrepareSlaterDeterminant. Sem essa ordenação, apenas a primeira rotação da sequência seria absorvida e as demais seriam sintetizadas com a decomposição quadrada completa.
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> # a reproducible random 6 x 6 orbital rotation
>>> rng = np.random.default_rng(1)
>>> z = rng.standard_normal((6, 6)) + 1j * rng.standard_normal((6, 6))
>>> q, r = np.linalg.qr(z)
>>> rotation = q * (np.diagonal(r) / np.abs(np.diagonal(r)))
>>>
>>> circuit = FermionicCircuit(6)
>>> circuit.append(InitializeModes([1, 1, 0, 0, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(rotation), circuit.modes)
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> prepared = pm.run(circuit)
>>> print(dict(sorted(prepared.count_ops().items())))
{'x': 2, 'xx_plus_yy': 8}
>>> prepared.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
Compare isso com a síntese da mesma rotação orbital isoladamente. Sem uma inicialização anterior, não há nada a fundir, então o preset recorre à decomposição quadrada completa, que é mais profunda e traz portas de fase diagonais:
>>> reference = FermionicCircuit(6)
>>> reference.append(OrbitalRotation(rotation), reference.modes)
>>> reference_prepared = pm.run(reference)
>>> print(dict(sorted(reference_prepared.count_ops().items())))
{'rz': 6, 'xx_plus_yy': 15}
>>> reference_prepared.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

A decomposição reduzida elimina as seis portas de fase e reduz quase à metade a quantidade de rotações de dois qubits. O determinante preparado está correto a menos de uma fase global (fisicamente irrelevante para a preparação de estados), que é o que as portas de fase teriam corrigido.
Duas maneiras de decompor a porta
Uma PrepareSlaterDeterminant pode ser decomposta de duas maneiras diferentes, e elas não são intercambiáveis:
decompose()
Expande a porta em sua definição: a InitializeModes da referência seguida da OrbitalRotation completa. Isso é equivalente à porta como operador, para qualquer estado de entrada.
PrepareSlaterDeterminant.synthesis, aplicada por F2FSynthesis
Substitui a porta pela decomposição reduzida deste guia. Isso é equivalente à porta apenas como preparação de estado: ela produz o mesmo determinante, a menos de uma fase global, quando os modos começam no vácuo. Ela é mais barata justamente porque assume isso.
Use a síntese quando a porta prepara um estado a partir do vácuo, que é para o que a porta serve e o que o transpilador faz por você. Use decompose() quando a porta precisa continuar atuando como operador, por exemplo porque outras portas a precedem nos mesmos modos, ou quando você quer inspecionar ou simular as portas simples. O passe F2FSynthesis não tem como saber com qual caso está lidando, então cabe ao circuito que você fornece preparar o determinante a partir do vácuo.
Simular um circuito com ffsim não exige nenhuma das duas: cada porta aplica seu unitário completo, então um circuito que contém uma PrepareSlaterDeterminant é simulado como está e permanece no caminho rápido para as portas OrbitalRotation ao redor dela. Decompor apenas a porta de Slater com circuit.decompose("PrepareSlaterDeterminant") também deixa essas rotações inteiras.
PrepareSlaterDeterminant, InitializeModes, OrbitalRotation, MergeOrbitalRotations, F2FSynthesis, FermionicSlaterGivensDecomposition, e o guia de LUCJ para o fluxo de trabalho que produz o padrão de inicialização global.
