Pular para o conteúdo principal

O ansatz

Assista enquanto Victoria Lipinska explica o que é um ansatz e por que ele é importante no contexto de um solucionador quântico variacional (VQE).

Referências

Os artigos a seguir são citados no vídeo acima.

Código do ansatz

Na lição anterior, você criou um Hamiltoniano que descreve a energia da molécula de interesse e o mapeou para um formato útil para um computador quântico. O VQE usa um circuito variacional para preparar estados quânticos. Em seguida, usamos esses estados para determinar o valor esperado do Hamiltoniano (a energia). Os parâmetros no circuito variacional são variados até que o cálculo convirja para um valor esperado mínimo. No contexto da química quântica, esse deve ser a energia do estado fundamental. Esta lição foca no circuito variacional, também chamado de ansatz (uma palavra alemã que significa "abordagem" ou "método").

Nesta lição você vai aprender:

  • o conjunto de ansätze pré-construídos disponíveis na biblioteca de circuitos
  • como especificar ou modificar as características de um ansatz
  • como construir seu próprio ansatz
  • exemplos de ansätze bons e ruins

A biblioteca de circuitos do Qiskit tem muitas categorias de circuitos que podem ser usados como ansatz. Aqui, vamos restringir nossa discussão a circuitos de duas localidades (circuitos compostos por portas que atuam em, no máximo, dois qubits de cada vez). O Efficient SU2 é um ansatz bastante utilizado.

Um circuito efficient_su_2 consiste em camadas de operações de qubit único abrangidas pelo SU(2) (grupo unitário especial de grau 2, como portas de rotação de Pauli) e entrelaçamentos CX. Esse é um padrão heurístico que pode ser útil em algoritmos quânticos variacionais como VQE e circuitos de classificação em aprendizado de máquina quântico (QML).

Vamos começar com um exemplo de circuito efficient_su2 de quatro qubits com dois tipos de portas SU(2), como rx e y. Também especificamos um esquema de entrelaçamento e o número de repetições. Se você simplesmente usar .draw() nos circuitos, obterá uma representação bastante abstrata. Um diagrama de circuito mais compreensível é obtido usando .decompose().draw(), e aqui usaremos output = "mpl".

# Added by doQumentation — required packages for this notebook
!pip install -q qiskit
from qiskit.circuit.library import efficient_su2

SU2_ansatz = efficient_su2(4, su2_gates=["rx", "y"], entanglement="linear", reps=1)
print(SU2_ansatz.draw())
SU2_ansatz.decompose().draw(output="mpl")
┌──────────┐┌───┐     ┌──────────┐   ┌───┐
q_0: ┤ Rx(θ[0]) ├┤ Y ├──■──┤ Rx(θ[4]) ├───┤ Y ├─────────────────────
├──────────┤├───┤┌─┴─┐└──────────┘┌──┴───┴───┐ ┌───┐
q_1: ┤ Rx(θ[1]) ├┤ Y ├┤ X ├─────■──────┤ Rx(θ[5]) ├───┤ Y ├─────────
├──────────┤├───┤└───┘ ┌─┴─┐ └──────────┘┌──┴───┴───┐┌───┐
q_2: ┤ Rx(θ[2]) ├┤ Y ├────────┤ X ├─────────■──────┤ Rx(θ[6]) ├┤ Y ├
├──────────┤├───┤ └───┘ ┌─┴─┐ ├──────────┤├───┤
q_3: ┤ Rx(θ[3]) ├┤ Y ├────────────────────┤ X ├────┤ Rx(θ[7]) ├┤ Y ├
└──────────┘└───┘ └───┘ └──────────┘└───┘

Saída da célula de código anterior

As portas SU(2) aparecem no início e no fim, com a ordem e os elementos especificados em su2_gates = [...]. O esquema de entrelaçamento linear significa que as portas CX percorrem os qubits numerados, entrelaçando 0 e 1, depois 1 e 2, e assim por diante, ao longo de uma linha diagonal no circuito. Como era de se esperar, definir reps = 2 simplesmente adiciona uma camada de entrelaçamento e uma camada SU(2) final. Definir reps = n corresponde a n camadas de entrelaçamento, com camadas SU(2) entre elas e em cada extremidade.

SU2_ansatz2 = efficient_su2(
4, su2_gates=["rx", "y", "z"], entanglement="linear", reps=2
)
SU2_ansatz2.decompose().draw(output="mpl")

Saída da célula de código anterior

Existem vários outros esquemas de entrelaçamento. Dois que valem destaque são circular e full. O entrelaçamento circular é idêntico ao linear, mas com uma porta CX adicional entrelaçando o primeiro e o último qubit. O esquema de entrelaçamento completo inclui uma porta CX entre cada par de qubits. Note que para um circuito de N qubits, isso equivale a N(N1)/2N(N-1)/2 portas CXCX, o que pode se tornar computacionalmente custoso.

SU2_ansatz3 = efficient_su2(
4, su2_gates=["rx", "y", "z"], entanglement="circular", reps=1
)
SU2_ansatz3.decompose().draw(output="mpl")

Saída da célula de código anterior

SU2_ansatz4 = efficient_su2(4, su2_gates=["rx", "y", "z"], entanglement="full", reps=1)
SU2_ansatz4.decompose().draw(output="mpl")

Saída da célula de código anterior

Você pode monitorar a profundidade do seu circuito usando .depth(), ou às vezes .decompose().depth().

print(SU2_ansatz4.decompose().depth())
11

Uma generalização do efficient_su2 é o circuito de duas localidades, que é em si um caso especial dos circuitos n-locais. Os circuitos de duas localidades também contêm blocos SU(2) (ou blocos de rotação) e blocos de entrelaçamento. Aqui, podemos especificar livremente o tipo de portas de entrelaçamento que queremos usar, por exemplo portas CRX. Neste exemplo, todas as portas aceitam um parâmetro, mas isso não é obrigatório. Por exemplo, pode-se usar portas de rotação Y e portas de entrelaçamento CX.

from qiskit.circuit.library import n_local

rotation_blocks = ["ry"]
entanglement_blocks = ["crx"]
two_ansatz = n_local(
4, rotation_blocks, entanglement_blocks, "linear", insert_barriers=True, reps=2
)
two_ansatz.decompose().draw(output="mpl")

Saída da célula de código anterior

O último ansatz que discutiremos por nome é o Pauli-two-design. Este circuito contém uma rotação inicial de RY(π/4)RY(\pi/4), e as camadas de rotação contêm rotações de Pauli de qubit único, onde o eixo é escolhido uniformemente de forma aleatória como X, Y ou Z. As camadas de entrelaçamento são compostas por pares de portas CZ com uma profundidade total de dois. Note a diferença na profundidade de entrelaçamento (e total do circuito) entre este pauli_two_design e, por exemplo, o efficient_su2.

from qiskit.circuit.library import pauli_two_design

PtwoD_ansatz = pauli_two_design(5, reps=1, seed=10599, insert_barriers=True)
PtwoD_ansatz.decompose().draw(output="mpl")

Saída da célula de código anterior

Esses circuitos variacionais pré-construídos são heurísticas úteis tanto para atingir um nível desejado de entrelaçamento quanto para limitar a profundidade do circuito. Mas não há nada de mágico neles. Você é livre para criar seu próprio circuito variacional. De fato, isso pode ser vantajoso em casos onde você sabe algo sobre o entrelaçamento do estado alvo do seu sistema.

Para criar seu próprio ansatz, você simplesmente constrói um circuito quântico com um subconjunto de portas sendo funções de elementos de um vetor de parâmetros ("theta" no exemplo de três qubits abaixo).

from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector

n = 3

theta = ParameterVector("θ", length=n)
qc = QuantumCircuit(n)
qc.h(0)
qc.h(2)
for i in range(n - 1):
qc.cx(i, i + 1)
qc.cz(0, n - 1)
qc.barrier()
for i in range(n):
qc.ry(theta[i], i)
qc.barrier()
qc.cz(0, n - 1)
for i in reversed(range(n - 1)):
qc.cx(i, i + 1)
qc.h(0)
qc.h(1)
own_ansatz = qc
print(own_ansatz.depth())
qc.draw("mpl")
9

Saída da célula de código anterior

Em geral, selecionar o melhor ansatz é uma arte; o melhor ansatz é aquele que te ajuda a atingir seu alvo com o menor número de etapas de otimização. É mais fácil identificar ansätze que provavelmente serão ruins. Por exemplo, maior profundidade de circuito tende a resultar em acúmulo de erros. A mitigação de erros pode ajudar com isso, mas é uma boa prática manter a profundidade do circuito tão baixa quanto razoável. Mas não omita o entrelaçamento que é necessário. Você pode ter um estado alvo que exige um esquema de entrelaçamento completo. Dois exemplos são mostrados abaixo que provavelmente são escolhas ruins por razões bastante óbvias. A escolha de um bom ansatz será revisitada em seções posteriores no contexto de testes de convergência.

Este primeiro circuito provavelmente é uma escolha ruim porque o último qubit não está entrelaçado com os outros de forma alguma. De fato, não há ação computacionalmente significativa no último qubit. Muito provavelmente, o último qubit deveria ser entrelaçado com os outros ou removido do cálculo.

n = 4

theta = ParameterVector("θ", length=n)
qc = QuantumCircuit(n)
qc.h(0)
qc.h(2)
for i in range(n - 2):
qc.cx(i, i + 1)
qc.cz(0, n - 2)
qc.barrier()
for i in range(n):
qc.ry(theta[i], i)
qc.barrier()
qc.cz(0, n - 2)
for i in reversed(range(n - 2)):
qc.cx(i, i + 1)
qc.h(0)
qc.h(1)
own_ansatz2 = qc
print(own_ansatz2.depth())
qc.draw("mpl")
9

Saída da célula de código anterior

Este último circuito provavelmente é uma escolha ruim porque a profundidade das portas é muito alta, e repetir a camada de entrelaçamento quatro vezes provavelmente não vai produzir uma correspondência substancialmente melhor ao estado alvo do que duas ou três repetições.

su2_ansatz_long = efficient_su2(
4, su2_gates=["rx", "y", "z"], entanglement="linear", reps=4
)
print(su2_ansatz_long.decompose().depth())
su2_ansatz_long.decompose().draw(output="mpl")
24

Saída da célula de código anterior

Esses circuitos não são "completos" no sentido de que ainda há parâmetros desconhecidos e variáveis a serem inseridos em muitas das portas. Esses parâmetros são escolhidos fazendo tentativas sucessivas e atualizando os parâmetros para reduzir o valor esperado da função de custo (no contexto da química, tipicamente a energia do estado fundamental). Em uma ou mesmo poucas dimensões, isso é trivial. Mas o circuito acima tem 20 parâmetros variacionais, o que significa que encontrar o estado alvo com a energia mínima implica buscar em um espaço de estado de 20 dimensões (outra razão para não incluir portas de circuito desnecessárias). É aqui que entram em jogo os algoritmos de otimização clássica, e esse é o assunto da próxima lição.