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?

Trabalhe com circuitos fermiônicos

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.

Este guia explica como usar o FermionicCircuit para implementar algoritmos quânticos diretamente no espaço fermiônico, em vez de mapear para Qubits primeiro.

Por que usar circuitos fermiônicos​

Considere o fluxo de trabalho tradicional para implementar a evolução temporal de um Hamiltoniano fermiônico em uma arquitetura baseada em Qubits:

  1. Defina o Hamiltoniano fermiônico (por exemplo, H=∑pqhpqcp†cqH = \sum_{pq} h_{pq} c^\dagger_p c_q).

  2. Mapeie-o para um operador de Qubits (por exemplo, usando o mapeamento de Jordan-Wigner).

  3. Implemente a evolução temporal no espaço de Qubits (usando Trotterização ou outros métodos).

Essa abordagem tem uma limitação fundamental: o mapeamento precoce para Qubits descarta o conhecimento do problema sobre a estrutura fermiônica. As passagens de otimização padrão não conseguem explorar simetrias fermiônicas nem reordenações permitidas de termos, resultando em circuitos subótimos, com profundidade e contagem de portas desnecessárias.

O FermionicCircuit possibilita um fluxo de trabalho melhor:

  1. Defina o Hamiltoniano fermiônico (por exemplo, H=∑pqhpqcp†cqH = \sum_{pq} h_{pq} c^\dagger_p c_q).

  2. Implemente a evolução temporal diretamente no espaço fermiônico usando portas fermiônicas.

  3. Mapeie para o espaço de Qubits como parte do processo de transpilação.

Essa abordagem resolve a limitação ao manter o conhecimento do problema no espaço fermiônico, onde as passagens de otimização podem explorar a estrutura fermiônica e as relações de comutação para reduzir a profundidade do circuito. O circuito fermiônico descreve qual computação executar, enquanto a transpilação cuida de como mapeá-la para Qubits, oferecendo uma separação de responsabilidades mais limpa.

Indexação genérica de modos​

Tanto o módulo operators quanto o FermionicCircuit usam indexação genérica baseada em modos, que não faz suposições sobre a natureza dos modos. Um modo é simplesmente um índice abstrato que rotula um grau de liberdade fermiônico, sem semântica inerente. Essa escolha de design espelha o módulo operators e garante máxima flexibilidade. Por exemplo, você pode implementar a evolução temporal de qualquer operador que implemente o protocolo OperatorTrait, independentemente de sua representação matemática.

Construa um circuito fermiônico​

O exemplo abaixo implementa a evolução temporal descrita na seção anterior. Ele constrói um FermionicCircuit especificando o número de modos fermiônicos e depois adiciona portas fermiônicas de qiskit_fermions.circuit.library para implementar a evolução temporal.

O exemplo também demonstra como incorporar conhecimento do domínio em um operador usando o agrupamento de termos do operador. Ao atribuir índices de grupo aos termos do Hamiltoniano, você preserva informações estruturais que as passagens de otimização podem explorar em toda a pilha de transpilação.

>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Create a circuit with 4 fermionic modes
>>> circuit = FermionicCircuit(4)
>>>
>>> # Define a simple fermionic Hamiltonian
>>> hamiltonian = FermionOperator.from_terms([
... ([cre(0), ann(2)], 0.5),
... ([cre(2), ann(0)], 0.5),
... ([cre(1), ann(3)], 0.5),
... ([cre(3), ann(1)], 0.5),
... ])
>>> # Add some problem structure by grouping our Hamiltonian terms
>>> hamiltonian.groups = [0, 0, 1, 1]
>>>
>>> # Add an evolution gate to implement exp(-i * t * H)
>>> evolution = Evolution(4, hamiltonian, time=1.0)
>>> circuit.append(evolution, circuit.register)

A simple `FermionicCircuit` with a single time evolution gate.

A decomposed `FermionicCircuit` with several time evolution gates.

Observe como o agrupamento de termos do operador é preservado mesmo em operações simples como a decomposição. Isso demonstra como a informação estrutural flui pela pilha de circuitos. Para entender a transpilação completa para Qubits, consulte o guia Transpilar circuitos fermiônicos.

Mantenha as portas separadas com uma barreira​

As passagens de otimização fundem portas vizinhas sempre que possível. Por exemplo, MergeOrbitalRotations combina uma sequência de portas OrbitalRotation consecutivas em uma só, e MergeSlaterDeterminantPreparation incorpora um InitializeModes na rotação que vem depois dele.

Use FermionicCircuit.barrier() quando quiser impedir isso. Uma barreira não tem efeito unitário, portanto não altera o estado que seu circuito prepara; ela apenas marca um ponto através do qual as passagens não fundem portas. Chame-a sem argumentos para abranger todos os modos, ou passe os modos aos quais restringi-la:

>>> from qiskit_fermions.circuit.library import OrbitalRotation
>>> import numpy as np
>>>
>>> rotations = FermionicCircuit(4)
>>> rotations.append(OrbitalRotation(np.eye(4, dtype=complex)), rotations.register)
>>> rotations.barrier()
>>> rotations.append(OrbitalRotation(np.eye(4, dtype=complex)), rotations.register)

A `FermionicCircuit` with a barrier separating two orbital rotations.

As barreiras sobrevivem à transpilação de férmion para Qubit, então também restringem a otimização em nível de Qubits que vem em seguida. O guia Transpilar circuitos fermiônicos cobre essa etapa.

Repita um circuito​

Muitos algoritmos aplicam o mesmo bloco de portas muitas vezes. A evolução temporal é o caso comum: um passo de Trotter é construído uma vez e depois repetido até atingir o tempo desejado. Use repeat() para construir o circuito repetido e compose() para juntá-lo a um prefixo de preparação de estado.

O exemplo abaixo constrói um passo de Trotter de segunda ordem para um modelo de Fermi-Hubbard. O passo é simétrico: ele divide o termo de salto em duas portas OrbitalRotation de meia duração que cercam a interação local.

>>> import scipy.linalg
>>> from qiskit_fermions.circuit.library import InitializeModes, OrbitalRotation
>>>
>>> norb, reps, time = 2, 3, 1.0
>>> num_modes = 2 * norb
>>> dt = time / reps
>>>
>>> hopping = np.zeros((norb, norb))
>>> hopping[0, 1] = hopping[1, 0] = -1.0
>>> interaction = FermionOperator.from_dict(
... {(cre(p), ann(p), cre(p + norb), ann(p + norb)): 2.0 for p in range(norb)}
... )
>>>
>>> def rotation(duration):
... propagator = scipy.linalg.expm(-1j * duration * hopping)
... # the same rotation acts on the alpha and beta halves of the register
... return scipy.linalg.block_diag(propagator, propagator)
>>>
>>> trotter_step = FermionicCircuit(num_modes)
>>> trotter_step.append(OrbitalRotation(rotation(dt / 2)), trotter_step.modes)
>>> trotter_step.append(
... Evolution(num_modes, interaction, dt, atomic=True), trotter_step.modes
... )
>>> trotter_step.append(OrbitalRotation(rotation(dt / 2)), trotter_step.modes)

Repetir o passo e compô-lo sobre uma referência de Hartree-Fock dá a evolução completa:

>>> evolution = FermionicCircuit(num_modes)
>>> evolution.append(InitializeModes([True, False, True, False]), evolution.modes)
>>> evolution.compose(trotter_step.repeat(reps), inplace=True)
>>> evolution.count_ops()["OrbitalRotation"]
6

A `FermionicCircuit` with repeated steps.

As repetições são achatadas no circuito retornado, em vez de embrulhadas em uma única porta, então count_ops() conta duas rotações por passo. Esse achatamento é o que torna visível ao transpilador a redundância nas fronteiras dos passos: a rotação final de meia duração de um passo e a inicial do próximo são adjacentes, então MergeOrbitalRotations as combina em uma única rotação.

>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.passes import MergeOrbitalRotations
>>>
>>> merged = FermionicPassManager([MergeOrbitalRotations()]).run(evolution)
>>> merged.count_ops()["OrbitalRotation"]
4

A `FermionicCircuit` with repeated steps and merged orbital rotations.

O resultado custa uma rotação por passo mais uma, que é o que você obtém ao fundir esses meios passos manualmente. Escrever o passo em sua forma simétrica natural e repeti-lo produz o mesmo circuito, então a fusão fica a cargo da etapa de otimização.

Quando precisar manter as repetições separadas, passe insert_barriers=True para colocar uma barrier() entre elas. As passagens de otimização não fundem portas através de uma barreira, então toda rotação sobrevive:

>>> separated = trotter_step.repeat(reps, insert_barriers=True)
>>> FermionicPassManager([MergeOrbitalRotations()]).run(separated).count_ops()[
... "OrbitalRotation"
... ]
6

A `FermionicCircuit` with repeated steps separated by barriers.

Nota

O argumento reps de FermionicSuzukiTrotter é um conceito diferente. Ele subdivide uma única porta Evolution em fatores mais numerosos e mais curtos, enquanto repeat() duplica circuitos inteiros. Repetir um circuito que contém uma única Evolution apenas multiplica seu tempo total de evolução.

Meça modos fermiônicos​

measure_all() lê cada modo em um bit clássico próprio, associando o modo i ao bit i. Use measure() com seu próprio ClassicalRegister para ler apenas alguns deles.

>>> measured = trotter_step.measure_all(inplace=False)
>>> measured.count_ops()["measure"]
4

Passar inplace=False retorna uma cópia com medição e deixa o original intacto, o que importa porque uma medição não é unitária: um circuito que contém uma não pode mais ser aplicado a um vetor de estado, então mantenha um circuito sem medição à mão se também quiser simulá-lo.

Medir aqui, em vez de no QuantumCircuit transpilado, é o que mantém as bitstrings amostradas fáceis de ler. Uma medição fermiônica nomeia o modo que ela lê, então toda etapa que move modos de lugar (RelabelModes no nível fermiônico, layout e roteamento no nível de Qubits) leva a medição junto, e a bitstring continua indexada pelo modo sobre o qual você perguntou.

Nota

Ao contrário de measure_all(), nenhuma barrier() é inserida antes das medições. Chame barrier() você mesmo antes, se quiser uma ali.

Se um bit medido significa o que você espera depende da codificação de férmion para Qubit, que é escolhida durante a transpilação, e não aqui. Sob uma codificação na base de ocupação, como Jordan-Wigner, cada modo é carregado por um Qubit e o bit é a ocupação daquele modo; veja FermionicMeasure.

Transpile circuitos fermiônicos​

Para implementar o algoritmo quântico representado pelo seu FermionicCircuit, ele precisa ser transpilado para um QuantumCircuit. Você pode aprender como fazer isso no guia Transpilar circuitos fermiônicos.

Próximos passos​