Pular para o conteúdo principal

Unindo tudo com o Qiskit Runtime

Resumo

Victoria Lipinska faz uma recapitulação final do que aprendemos até agora.

Referências

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

VQE com padrões do Qiskit

Temos todos os componentes necessários para um cálculo VQE:

  • Hamiltoniano
  • Ansatz
  • Otimizador clássico

Agora basta combiná-los no framework de padrões do Qiskit.

Etapa 1: Mapear entradas clássicas para um problema quântico

Como mencionado anteriormente, assumiremos aqui que um Hamiltoniano de interesse devidamente formatado já foi gerado. Se tiver dúvidas sobre isso, consulte a lição sobre Hamiltonianos para orientação. O bloco de código abaixo configura os componentes explicados nas lições anteriores. Aqui escolhemos modelar o H2 porque seu Hamiltoniano é compacto o suficiente para ser escrito por extenso.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-aer qiskit-ibm-runtime scipy
# General imports
import numpy as np
from qiskit.quantum_info import SparsePauliOp

# Hamiltonian obtained from a previous lesson

H = SparsePauliOp(
[
"IIII",
"IIIZ",
"IZII",
"IIZI",
"ZIII",
"IZIZ",
"IIZZ",
"ZIIZ",
"IZZI",
"ZZII",
"ZIZI",
"YYYY",
"XXYY",
"YYXX",
"XXXX",
],
coeffs=[
-0.09820182 + 0.0j,
-0.1740751 + 0.0j,
-0.1740751 + 0.0j,
0.2242933 + 0.0j,
0.2242933 + 0.0j,
0.16891402 + 0.0j,
0.1210099 + 0.0j,
0.16631441 + 0.0j,
0.16631441 + 0.0j,
0.1210099 + 0.0j,
0.17504456 + 0.0j,
0.04530451 + 0.0j,
0.04530451 + 0.0j,
0.04530451 + 0.0j,
0.04530451 + 0.0j,
],
)

nuclear_repulsion = 0.7199689944489797

Selecionamos um circuito efficient_su2 e o otimizador COBYLA para começar.

# Pre-defined ansatz circuit
from qiskit.circuit.library import efficient_su2

# SciPy minimizer routine
from scipy.optimize import minimize

# Plotting functions

# Random initial state and efficient_su2 ansatz
ansatz = efficient_su2(H.num_qubits, su2_gates=["rx"], entanglement="linear", reps=1)
x0 = 2 * np.pi * np.random.random(ansatz.num_parameters)
print(ansatz.decompose().depth())
ansatz.decompose().draw("mpl")
5

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

Agora construímos nossa função de custo. Ela está obviamente relacionada ao Hamiltoniano, mas difere no sentido de que o Hamiltoniano é um operador, e queremos uma função que retorne o valor esperado desse operador usando o Estimator. Claro, isso é obtido usando o ansatz e os parâmetros variacionais, então todos eles aparecem como argumentos. Abaixo, definimos versões ligeiramente diferentes para uso em hardware real ou simuladores.

def cost_func(params, ansatz, H, estimator):
pub = (ansatz, [H], [params])
result = estimator.run(pubs=[pub]).result()
energy = result[0].data.evs[0]
return energy

# def cost_func_sim(params, ansatz, H, estimator):
# energy = estimator.run(ansatz, H, parameter_values=params).result().values[0]
# return energy

Etapa 2: Otimizar o problema para execução quântica

Queremos que nosso código seja executado com a maior eficiência possível no hardware que usamos. Portanto, devemos selecionar um backend para iniciar a etapa de otimização. O código abaixo seleciona o backend menos ocupado disponível para você.

# To run on hardware, select the backend with the fewest number of jobs in the queue
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService(channel="ibm_quantum_platform")
backend = service.least_busy(operational=True, simulator=False)
backend.name

Otimizar o circuito para execução em um backend real é um tópico rico e crítico. Mas não é específico do VQE. Por ora, vamos apenas relembrar dois termos importantes:

  • optimization_level: descreve o quão bem o circuito é adaptado ao layout do backend selecionado. O nível de otimização mais baixo faz apenas o mínimo necessário para que o circuito rode no dispositivo: mapeia os qubits do circuito para os qubits do dispositivo e adiciona portas swap para permitir todas as operações de dois qubits. O nível de otimização mais alto é muito mais inteligente e usa muitos truques para reduzir o número total de portas. Como as portas de múltiplos qubits têm altas taxas de erro e os qubits se decoerênciam com o tempo, circuitos mais curtos devem dar melhores resultados.
  • Dynamical Decoupling: podemos aplicar uma sequência de portas a qubits ociosos. Isso cancela algumas interações indesejadas com o ambiente. Consulte a documentação vinculada para mais informações sobre como otimizar circuitos. O código abaixo gera um gerenciador de passes usando gerenciadores de passes predefinidos de qiskit.transpiler.
from qiskit.transpiler import PassManager
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.transpiler.passes import (
ALAPScheduleAnalysis,
PadDynamicalDecoupling,
ConstrainedReschedule,
)
from qiskit.circuit.library import XGate

target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
pm.scheduling = PassManager(
[
ALAPScheduleAnalysis(target=target),
ConstrainedReschedule(
acquire_alignment=target.acquire_alignment,
pulse_alignment=target.pulse_alignment,
target=target,
),
PadDynamicalDecoupling(
target=target,
dd_sequence=[XGate(), XGate()],
pulse_alignment=target.pulse_alignment,
),
]
)

# Use the pass manager and draw the resulting circuit
ansatz_isa = pm.run(ansatz)
ansatz_isa.draw(output="mpl", idle_wires=False, style="iqp")

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

Também precisamos aplicar as características de layout do dispositivo ao Hamiltoniano.

hamiltonian_isa = H.apply_layout(ansatz_isa.layout)
hamiltonian_isa
SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYYYII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYXXII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXYYII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXXXII'],
coeffs=[-0.09820182+0.j, -0.1740751 +0.j, -0.1740751 +0.j, 0.2242933 +0.j,
0.2242933 +0.j, 0.16891402+0.j, 0.1210099 +0.j, 0.16631441+0.j,
0.16631441+0.j, 0.1210099 +0.j, 0.17504456+0.j, 0.04530451+0.j,
0.04530451+0.j, 0.04530451+0.j, 0.04530451+0.j])

Etapa 3: Executar usando os Primitivos do Qiskit

Antes de executar no hardware selecionado, é uma boa ideia usar um simulador para depuração rápida e, às vezes, para estimativas de erro. Por essas razões, mostramos brevemente como executar o VQE em um simulador. Mas é fundamental notar que nenhum computador clássico, simulador ou GPU consegue simular com precisão a funcionalidade completa de um computador quântico de 127 qubits altamente entrelaçado. Na era atual da utilidade quântica, os simuladores terão uso limitado.

Lembre-se de que para cada escolha de parâmetros no circuito variacional, um valor esperado precisa ser calculado (pois esse é o valor a ser minimizado). Como você já deve ter percebido, a maneira mais eficiente de fazer isso é usar o primitivo do Qiskit, o Estimator. Vamos começar usando um simulador local, o que exigirá que usemos a versão local do Estimator chamada BackendEstimator.

Mantendo o backend real que usamos para otimização, podemos importar um modelo do comportamento de ruído daquele dispositivo para usar com o simulador local de nossa escolha. Aqui, usaremos o aer_simulator_statevector.

# We will start by using a local simulator
from qiskit_aer import AerSimulator

# Import an estimator, this time from qiskit (we will import from Runtime for real hardware)
from qiskit.primitives import BackendEstimatorV2

# generate a simulator that mimics the real quantum system
backend_sim = AerSimulator.from_backend(backend)
estimator = BackendEstimatorV2(backend=backend_sim)

Chegou a hora de implementar o VQE, minimizando a função de custo usando o Hamiltoniano selecionado, o ansatz, o otimizador clássico e nosso BackendEstimator, baseado no backend real que selecionamos para uso subsequente. Note que aqui escolhemos um número relativamente pequeno para o máximo de iterações. Isso porque estamos usando o simulador apenas para depuração. As etapas de otimização do VQE frequentemente exigem centenas de iterações para convergir.

res = minimize(
cost_func,
x0,
args=(ansatz_isa, hamiltonian_isa, estimator),
method="cobyla",
options={"maxiter": 10, "disp": True},
)

print(getattr(res, "fun") - nuclear_repulsion)
print(res)
Return from COBYLA because the objective function has been evaluated MAXFUN times.
Number of function values = 10 Least value of F = -0.11556938907226563
The corresponding X is:
[4.11796514 4.52126324 0.69570423 4.12781503 6.55507846 1.80713073
0.9645473 6.23812214]

-0.8355383835212453
message: Return from COBYLA because the objective function has been evaluated MAXFUN times.
success: False
status: 3
fun: -0.11556938907226563
x: [ 4.118e+00 4.521e+00 6.957e-01 4.128e+00 6.555e+00
1.807e+00 9.645e-01 6.238e+00]
nfev: 10
maxcv: 0.0

O código foi executado corretamente, embora não tenha convergido, o que era esperado. Vamos prosseguir executando o cálculo em hardware real e depois discutir os resultados. Para backends reais, usaremos o Estimator do Qiskit Runtime. Vamos querer executar isso dentro de uma sessão do Qiskit Runtime e, em geral, vamos querer especificar opções para essa sessão.

from qiskit_ibm_runtime import QiskitRuntimeService, Session
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime.options import EstimatorOptions

Entre outras coisas, usar uma sessão significa que nosso job ficará na fila apenas uma vez, para começar. As iterações subsequentes do otimizador clássico não serão enfileiradas. Na sessão, podemos definir níveis de resiliência e otimização. Essas ferramentas são importantes o suficiente para incluirmos uma breve revisão de cada uma e sua importância no VQE, com links para aprender mais:

  • Sessões do Runtime: o VQE é inerentemente iterativo, com o otimizador clássico selecionando novos parâmetros variacionais e, portanto, novas portas sendo usadas em cada tentativa subsequente. Sem usar sessões, isso poderia resultar em tempo de fila adicional entre cada circuito de tentativa. Encapsular o cálculo VQE dentro de uma sessão resulta em apenas uma fila inicial antes do início do job, sem tempo de fila adicional entre as etapas variacionais. Essa estratégia já foi usada no exemplo da lição anterior, mas pode desempenhar um papel ainda mais importante ao variar a geometria. Para mais sobre sessões, consulte a documentação sobre modos de execução.
  • Otimização integrada do Estimator: no Estimator há opções integradas para otimizar um cálculo. Em muitos contextos (incluindo o Estimator), as configurações são limitadas a 0 e 1, onde 0 indica nenhuma otimização e 1 (padrão) indica alguma otimização do seu circuito para o hardware selecionado. Alguns outros contextos permitem configurações de 0, 1, 2 ou 3. Para mais informações sobre os métodos específicos usados em diferentes configurações, consulte a documentação. Aqui, vamos definir a otimização como 0 e usar 'skip_transpilation = true', pois já transpilamos nosso circuito usando o gerenciador de passes acima, na seção de otimização.
  • Resiliência integrada do Estimator: assim como a otimização, o Estimator tem configurações integradas de resiliência contra erros, correspondendo a diferentes abordagens para mitigação de erros. Para aprender sobre as configurações de nível de resiliência, consulte a documentação.

Vale notar que a mitigação de erros desempenha um papel sutil na convergência de um cálculo VQE. O otimizador clássico está buscando no espaço de parâmetros aqueles que minimizam a energia. Quando você está muito longe dos parâmetros ótimos, um gradiente acentuado pode ser aparente para o otimizador clássico mesmo na presença de erros. Mas conforme o cálculo converge e você se aproxima dos valores ótimos, o gradiente fica menor e mais facilmente mascarado pelos erros. Quanto de mitigação de erros você quer usar? Em quais pontos da convergência? Essas são escolhas que você precisa fazer para o seu caso de uso específico.

Para esta primeira execução em hardware, definimos a resiliência como 0 para facilitar uma execução relativamente rápida. Para qualquer aplicação séria, você vai querer usar mitigação de erros. Note que na célula abaixo há dois conjuntos de opções: (1) opções para a sessão do Runtime, que nomeamos "session_options", e (2) opções para o otimizador clássico, simplesmente chamadas de "options" aqui.

estimator_options = EstimatorOptions(resilience_level=0, default_shots=2000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)

res = minimize(
cost_func,
x0,
args=(ansatz_isa, hamiltonian_isa, estimator),
method="cobyla",
options={"maxiter": 10, "disp": True},
)
Return from COBYLA because the objective function has been evaluated MAXFUN times.
Number of function values = 10 Least value of F = -0.11691688904
The corresponding X is:
[5.11796514 5.52126324 0.69570423 5.12781503 6.55507846 1.80713073
1.9645473 6.23812214]

Você pode acompanhar o progresso do seu job na Plataforma IBM Quantum® em Workloads.

print(getattr(res, "fun") - nuclear_repulsion)
print(res)
-0.8368858834889796
message: Return from COBYLA because the objective function has been evaluated MAXFUN times.
success: False
status: 3
fun: -0.11691688904
x: [ 5.118e+00 5.521e+00 6.957e-01 5.128e+00 6.555e+00
1.807e+00 1.965e+00 6.238e+00]
nfev: 10
maxcv: 0.0

Etapa 4: Pós-processar e retornar o resultado em formato clássico

Vamos parar um momento para garantir que entendemos esses resultados. O resultado "fun" é o valor mínimo que obtivemos para a função de custo (não necessariamente o último valor calculado). Essa é a energia total, incluindo a repulsão nuclear positiva, por isso também definimos electron_energy.

No caso acima, temos uma mensagem informando que o número máximo de avaliações da função foi excedido e que o número de avaliações da função (nfev) foi 10. Isso simplesmente significa que outros critérios de convergência da otimização não foram atendidos; em outras palavras, não há razão para pensar que encontramos a energia do estado fundamental. Esse também é o significado de success ser "False".

Por fim, temos x. Esse é o vetor de parâmetros variacionais. Esses são os parâmetros usados no cálculo que produziu a menor função de custo (valor esperado de energia). Esses oito valores correspondem aos oito ângulos de rotação nas portas do ansatz que aceitam ângulos de rotação variáveis.

Parabéns! Você executou um cálculo VQE em uma QPU do IBM Quantum!

Na próxima lição, veremos como ajustar esse fluxo de trabalho para incluir variáveis no seu Hamiltoniano. No contexto de problemas de química quântica, isso pode significar variar a geometria para determinar formatos de moléculas ou sítios de ligação.

import qiskit
import qiskit_ibm_runtime

print(qiskit.version.get_version_info())
print(qiskit_ibm_runtime.version.get_version_info())
2.1.0
0.40.1