Funções de custo
Nesta lição, aprenderemos a avaliar uma função de custo :
- Primeiro, vamos aprender sobre as primitivas d IBM Quantum
- Defina uma função de custo . Essa é uma função específica do problema que define a meta do problema a ser minimizada (ou maximizada) pelo otimizador
- Definição de uma estratégia de medição com as primitivas do IBM Quantum para otimizar a relação entre velocidade e precisão
Primitivas
Todos os sistemas físicos, sejam eles clássicos ou quânticos, podem existir em diferentes estados. Por exemplo, um carro em uma estrada pode ter uma determinada massa, posição, velocidade ou aceleração que caracterizam seu estado. Da mesma forma, os sistemas quânticos também podem ter configurações ou estados diferentes, mas eles diferem dos sistemas clássicos na forma como lidamos com as medições e a evolução do estado. Isso leva a propriedades únicas, como a superposição e o emaranhamento, que são exclusivas da mecânica quântica. Assim como podemos descrever o estado de um carro usando propriedades físicas como velocidade ou aceleração, também podemos descrever o estado de um sistema quântico usando observáveis, que são objetos matemáticos.
Na mecânica quântica, os estados são representados por vetores de coluna complexos normalizados, ou kets ( ), e os observáveis são operadores lineares hermitianos ( ) que atuam nos kets. Um vetor próprio ( ) de um observável é conhecido como estado próprio. A medição de um observável para um de seus estados próprios ( ) nos dará o valor próprio correspondente ( ) como leitura.
Se você estiver se perguntando como medir um sistema quântico e o que pode medir, o Qiskit oferece dois sites Operações básicas e fundamentais ou tipo de dados. O Qiskit tem os primitivos Sampler e Estimator para servir como blocos de construção para construir facilmente cargas de trabalho algorítmicas complexas. que podem ajudar:
Sampler: Dado um estado quântico , essa primitiva obtém a probabilidade de cada estado possível da base computacional.Estimator: Dado um observável quântico e um estado , essa primitiva calcula o valor esperado de .
A primitiva Sampler
A primitiva Sampler calcula a probabilidade de obter cada estado possível a partir da base computacional, dado um circuito quântico que prepara o estado . Ela calcula
Onde é o número de qubits e a representação inteira de qualquer cadeia binária de saída possível (ou seja, a base de inteiros ).
O IBM Quantum Sampler percorre o circuito várias vezes em um dispositivo quântico, realizando medições a cada passagem e reconstruindo a distribuição de probabilidade a partir das sequências de bits recuperadas. Quanto mais execuções (ou tentativas ) forem realizadas, mais precisos serão os resultados, mas isso exige mais tempo e recursos quânticos.
Entretanto, como o número de saídas possíveis cresce exponencialmente com o número de qubits (ou seja, ), o número de disparos também precisará crescer exponencialmente para capturar uma distribuição de probabilidade densa. Portanto, o Sampler só é eficiente para distribuições de probabilidade esparsas; onde o estado de destino deve ser expressável como uma combinação linear dos estados da base computacional, com o número de termos crescendo no máximo polinomialmente com o número de qubits:
O site Sampler também pode ser configurado para recuperar probabilidades de uma subseção do circuito, representando um subconjunto do total de estados possíveis.
A primitiva Estimador
A primitiva Estimator calcula o valor da expectativa de um observável para um estado quântico ; onde as probabilidades do observável podem ser expressas como , sendo os estados próprios do observável . O valor da expectativa é então definido como a média de todos os resultados possíveis (ou seja, os valores próprios do observável) de uma medição do estado , ponderada pelas probabilidades correspondentes:
No entanto, nem sempre é possível calcular o valor esperado de um observável, pois muitas vezes não conhecemos sua base de eigenfunções. O método de decomposição em bases próprias ( IBM Quantum ) Estimator utiliza um processo algébrico complexo para estimar o valor esperado em um dispositivo quântico real, decompondo o observável em uma combinação de outros observáveis cujas bases próprias já conhecemos.
Em termos mais simples, o Estimator decompõe qualquer observável que ele não sabe como medir em observáveis mais simples e mensuráveis chamados Conjunto de matrizes comumente usadas na computação quântica para representar e manipular estados quânticos, consistindo na matriz de identidade e nas três matrizes de Pauli (X, Y e Z)..
Qualquer operador pode ser expresso como uma combinação de operadores Pauli.
tal qual
em que é o número de qubits, para (ou seja, a base de números inteiros ) e .
Depois de realizar essa decomposição, o site Estimator deriva um novo circuito para cada observável (do circuito original), para efetivamente diagonalizar o observável de Pauli na base computacional e medi-lo. Podemos medir facilmente os observáveis de Pauli porque sabemos com antecedência, o que geralmente não acontece com outros observáveis.
Para cada , o Estimator executa o circuito correspondente em um dispositivo quântico várias vezes, mede o estado de saída na base computacional e calcula a probabilidade de obter cada saída possível . Em seguida, ele procura o valor próprio de correspondente a cada saída , multiplica por e soma todos os resultados para obter o valor esperado do observável para o estado dado .
Como o cálculo do valor de expectativa de Paulis é impraticável (ou seja, cresce exponencialmente), Estimator só pode ser eficiente quando uma grande quantidade de é zero (ou seja, decomposição de Pauli esparsa em vez de densa ). Formalmente, dizemos que, para que esse cálculo possa ser resolvido com eficiência, o número de termos diferentes de zero deve crescer no máximo polinomialmente com o número de qubits :
O leitor pode notar a suposição implícita de que a probabilidade O processo de fazer várias medições de uma ou várias coisas. também precisa ser eficiente, conforme explicado para Sampler, o que significa
Exemplo guiado para calcular valores esperados
Vamos supor o estado de um único qubit , e observável
com o seguinte valor de expectativa teórica
Como não sabemos como medir esse observável, não podemos calcular seu valor de expectativa diretamente e precisamos expressá-lo novamente como . O que pode ser demonstrado como o mesmo resultado em virtude da observação de que , e .
Vamos ver como calcular e diretamente. Como e não se combinam (ou seja, não compartilham a mesma base própria), eles não podem ser medidos simultaneamente e, portanto, precisamos dos circuitos auxiliares:
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
# The following code will work for any other initial single-qubit state and observable
original_circuit = QuantumCircuit(1)
original_circuit.h(0)
H = SparsePauliOp(["X", "Z"], [2, -1])
aux_circuits = []
for pauli in H.paulis:
aux_circ = original_circuit.copy()
aux_circ.barrier()
if str(pauli) == "X":
aux_circ.h(0)
elif str(pauli) == "Y":
aux_circ.sdg(0)
aux_circ.h(0)
else:
aux_circ.id(0)
aux_circ.measure_all()
aux_circuits.append(aux_circ)
original_circuit.draw("mpl")Output:
# Auxiliary circuit for X
aux_circuits[0].draw("mpl")Output:
# Auxiliary circuit for Z
aux_circuits[1].draw("mpl")Output:
Agora podemos realizar o cálculo manualmente usando Sampler e verificar os resultados em Estimator:
from qiskit.primitives import StatevectorSampler, StatevectorEstimator
from qiskit.result import QuasiDistribution
import numpy as np
## SAMPLER
shots = 10000
sampler = StatevectorSampler()
job = sampler.run(aux_circuits, shots=shots)
# Run the sampler job and step through results
expvals = []
for index, pauli in enumerate(H.paulis):
data_pub = job.result()[index].data
bitstrings = data_pub.meas.get_bitstrings()
counts = data_pub.meas.get_counts()
quasi_dist = QuasiDistribution(
{outcome: freq / shots for outcome, freq in counts.items()}
)
# Use the probabilities and known eigenvalues of Pauli operators to estimate
# the expectation value.
val = 0
if str(pauli) == "X":
val += -1 * quasi_dist.get(1, 0)
val += 1 * quasi_dist.get(0, 0)
if str(pauli) == "Y":
val += -1 * quasi_dist.get(1, 0)
val += 1 * quasi_dist.get(0, 0)
if str(pauli) == "Z":
val += 1 * quasi_dist.get(0, 0)
val += -1 * quasi_dist.get(1, 0)
expvals.append(val)
# Print expectation values
print("Sampler results:")
for pauli, expval in zip(H.paulis, expvals):
print(f" >> Expected value of {str(pauli)}: {expval:.5f}")
total_expval = np.sum(H.coeffs * expvals).real
print(f" >> Total expected value: {total_expval:.5f}")
# Use estimator for comparison
observables = [
*H.paulis,
H,
] # Note: run for individual Paulis as well as full observable H
estimator = StatevectorEstimator()
job = estimator.run([(original_circuit, observables)])
estimator_expvals = job.result()[0].data.evs
# Print results
print("Estimator results:")
for obs, expval in zip(observables, estimator_expvals):
if obs is not H:
print(f" >> Expected value of {str(obs)}: {expval:.5f}")
else:
print(f" >> Total expected value: {expval:.5f}")Output:
Sampler results:
>> Expected value of X: 1.00000
>> Expected value of Z: 0.00420
>> Total expected value: 1.99580
Estimator results:
>> Expected value of X: 1.00000
>> Expected value of Z: 0.00000
>> Total expected value: 2.00000
Rigor matemático (opcional)
Expressando com relação à base de estados próprios de , , segue-se:
Como não conhecemos os valores próprios ou os estados próprios do observável alvo , primeiro precisamos considerar sua diagonalização. Dado que é Um hermitiano é uma matriz quadrada que é igual à sua própria transposição conjugada ou um operador linear que é autoadjunto., existe uma transformação unitária de modo que onde é a matriz diagonal de valores próprios, portanto se , e .
Isso implica que o valor esperado pode ser reescrito como:
Considerando que, se um sistema estiver no estado , a probabilidade de medir é , o valor esperado acima pode ser expresso como:
É muito importante observar que as probabilidades são obtidas do estado em vez de . É por isso que a matriz é absolutamente necessária.
Você deve estar se perguntando como obter a matriz e os valores próprios . Se você já tivesse os valores próprios, não haveria necessidade de usar um computador quântico, pois o objetivo dos algoritmos variacionais é encontrar esses valores próprios de .
Felizmente, há uma maneira de contornar isso: qualquer matriz pode ser escrita como uma combinação linear de produtos tensoriais de matrizes de Pauli e identidades, sendo que todas elas são hermitianas e unitárias, com e conhecidos. É isso que o IBM Quantum Estimator faz internamente, decompondo qualquer objeto Operator em um SparsePauliOp.
Aqui estão os operadores que podem ser usados:
Portanto, vamos reescrever com relação à Paulis e às identidades:
sendo para (ou seja, a base ) e :
onde e , de modo que:
Funções de custo
Em geral, as funções de custo são usadas para descrever a meta de um problema e o desempenho de um estado de teste em relação a essa meta. Essa definição pode ser aplicada a vários exemplos em química, aprendizado de máquina, finanças, otimização e assim por diante.
Vamos considerar um exemplo simples de como encontrar o estado fundamental de um sistema. Nosso objetivo é minimizar o valor da expectativa do observável que representa a energia (Hamiltoniano ):
Podemos usar o site Estimator para avaliar o valor da expectativa e passar esse valor para um otimizador para minimizá-lo. Se a otimização for bem-sucedida, ela retornará um conjunto de valores de parâmetros ideais , a partir do qual poderemos construir o estado da solução proposta e calcular o valor da expectativa observada como .
Observe como só conseguiremos minimizar a função de custo para o conjunto limitado de estados que estamos considerando. Isso nos leva a duas possibilidades distintas:
- Nosso ansatz não define o estado da solução em todo o espaço de pesquisa : Se esse for o caso, nosso otimizador nunca encontrará a solução, e precisamos experimentar outras ansatzes que possam representar nosso espaço de pesquisa com mais precisão.
- Nosso otimizador não consegue encontrar essa solução válida : A otimização pode ser definida globalmente e definida localmente. Exploraremos o que isso significa na seção posterior.
Em suma, estaremos executando um loop de otimização clássica, mas confiando a avaliação da função de custo a um computador quântico. Nessa perspectiva, poderíamos pensar na otimização como um esforço puramente clássico em que chamamos alguns Um dispositivo hipotético ou componente de software que executa uma função específica, mas cujo funcionamento interno é desconhecido. Um usuário está ciente apenas das entradas e saídas da caixa preta e não tem conhecimento de como a caixa preta processa as entradas para produzir as saídas. cada vez que o otimizador precisa avaliar a função de custo.
def cost_func_vqe(params, circuit, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
pub = (circuit, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
return costfrom qiskit.circuit.library import TwoLocal
observable = SparsePauliOp.from_list([("XX", 1), ("YY", -3)])
reference_circuit = QuantumCircuit(2)
reference_circuit.x(0)
variational_form = TwoLocal(
2,
rotation_blocks=["rz", "ry"],
entanglement_blocks="cx",
entanglement="linear",
reps=1,
)
ansatz = reference_circuit.compose(variational_form)
theta_list = (2 * np.pi * np.random.rand(1, 8)).tolist()
ansatz.decompose().draw("mpl")Output:
Primeiro, faremos isso usando um simulador: o StatevectorEstimator. Isso geralmente é aconselhável para a depuração, mas seguiremos imediatamente a execução da depuração com um cálculo no hardware quântico real. Cada vez mais, os problemas de interesse não são mais simuláveis de forma clássica sem instalações de supercomputação de última geração.
estimator = StatevectorEstimator()
cost = cost_func_vqe(theta_list, ansatz, observable, estimator)
print(cost)Output:
[-0.58744589]
Agora vamos prosseguir com a execução em um computador quântico real. Observe as mudanças na sintaxe. As etapas relacionadas ao pass_manager serão abordadas com mais detalhes no próximo exemplo. Uma etapa de particular importância nos algoritmos variacionais é o uso de uma sessão do Serviço de Computação do IBM Quantum. Iniciar uma sessão permite executar várias iterações de um algoritmo variacional sem precisar aguardar em uma nova fila cada vez que os parâmetros são atualizados. Isso é importante se os tempos de espera forem longos e/ou forem necessárias muitas iterações. Observe que os usuários do Open Plan não podem utilizar sessões do Quantum Compute. Caso você não tenha acesso às sessões, pode reduzir o número de iterações enviadas de cada vez e salvar os parâmetros mais recentes para uso em execuções futuras. Se você enviar muitas iterações ou se os tempos de espera na fila forem muito longos, poderá receber o código de erro 1217, que se refere a longos atrasos entre os envios de tarefas.
# Estimated usage: < 1 min. Benchmarked at 7 seconds on an Eagle processor
# Load necessary packages:
from qiskit_ibm_runtime import (
QiskitRuntimeService,
Session,
EstimatorOptions,
EstimatorV2 as Estimator,
)
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
# Select the least busy backend:
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
)
# Or get a specific backend:
# backend = service.backend("ibm_brisbane")
# Use a pass manager to transpile the circuit and observable for the specific backend being used:
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_ansatz = pm.run(ansatz)
isa_observable = observable.apply_layout(layout=isa_ansatz.layout)
# Set estimator options
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
# Open a Quantum Compute session:
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
cost = cost_func_vqe(theta_list, isa_ansatz, isa_observable, estimator)
session.close()
print(cost)Observe que os valores obtidos com os dois cálculos acima são muito semelhantes. As técnicas para melhorar os resultados serão discutidas mais adiante.
Exemplo de mapeamento para sistemas não físicos
O problema do corte máximo (max-cut) é um problema de otimização combinatória que consiste em dividir os vértices de um grafo em dois conjuntos disjuntos, de modo a maximizar o número de arestas entre os dois conjuntos. De forma mais formal, dado um grafo não direcionado , em que é o conjunto de vértices e é o conjunto de arestas, o problema do corte máximo consiste em dividir os vértices em dois subconjuntos disjuntos, e , de modo a maximizar o número de arestas cuja uma extremidade pertença a e a outra a .
Podemos aplicar o método max-cut para resolver diversos problemas, incluindo: agrupamento, projeto de redes, transições de fase e assim por diante. Vamos começar criando um gráfico do problema:
import rustworkx as rx
from rustworkx.visualization import mpl_draw
n = 4
G = rx.PyGraph()
G.add_nodes_from(range(n))
# The edge syntax is (start, end, weight)
edges = [(0, 1, 1.0), (0, 2, 1.0), (0, 3, 1.0), (1, 2, 1.0), (2, 3, 1.0)]
G.add_edges_from(edges)
mpl_draw(
G, pos=rx.shell_layout(G), with_labels=True, edge_labels=str, node_color="#1192E8"
)Output:
Esse problema pode ser expresso como um problema de otimização binária. Para cada nó , em que é o número de nós do gráfico (nesse caso, ), consideraremos a variável binária . Essa variável terá o valor se o nó for um dos grupos que rotularemos como e se estiver no outro grupo, que rotularemos como . Também denotaremos como (elemento da matriz de adjacência ) o peso da borda que vai do nó ao nó . Como o gráfico não é direcionado, . Então, podemos formular nosso problema como a maximização da seguinte função de custo:
Para resolver esse problema com um computador quântico, vamos expressar a função de custo como o valor esperado de um observável. Entretanto, os observáveis que o Qiskit admite nativamente consistem em operadores Pauli, que têm autovalores e em vez de e . É por isso que faremos a seguinte mudança de variável:
Onde . Podemos usar a matriz de adjacência para acessar confortavelmente os pesos de todas as bordas. Isso será usado para obter nossa função de custo:
Isso implica que:
Portanto, a nova função de custo que queremos maximizar é:
Além disso, a tendência natural de um computador quântico é encontrar mínimos (geralmente a energia mais baixa) em vez de máximos, portanto, em vez de maximizar , vamos minimizar:
Agora que temos uma função de custo a ser minimizada, cujas variáveis podem ter os valores e , podemos fazer a seguinte analogia com o Pauli :
Em outras palavras, a variável será equivalente a uma porta atuando no qubit . Além disso:
Então, o observável que vamos considerar é:
ao qual teremos que adicionar o termo independente posteriormente:
O operador é uma combinação linear de termos com operadores Z em nós conectados por uma borda (lembre-se de que o 0º qubit é o mais à direita): . Depois que o operador é construído, o ansatz para o algoritmo QAOA pode ser facilmente construído usando o circuito QAOAAnsatz da biblioteca de circuitos Qiskit.
from qiskit.circuit.library import QAOAAnsatz
from qiskit.quantum_info import SparsePauliOp
hamiltonian = SparsePauliOp.from_list(
[("IIZZ", 1), ("IZIZ", 1), ("IZZI", 1), ("ZIIZ", 1), ("ZZII", 1)]
)
ansatz = QAOAAnsatz(hamiltonian, reps=2)
# Draw
ansatz.decompose(reps=3).draw("mpl")Output:
# Sum the weights, and divide by 2
offset = -sum(edge[2] for edge in edges) / 2
print(f"""Offset: {offset}""")Output:
Offset: -2.5
Como o Estimador de IBM Quantum a aceita diretamente um hamiltoniano e um ansatz parametrizado, retornando a energia necessária, a função de custo para uma instância de QAOA é bastante simples:
def cost_func(params, ansatz, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
pub = (ansatz, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
# cost = estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]
return costimport numpy as np
x0 = 2 * np.pi * np.random.rand(ansatz.num_parameters)
estimator = StatevectorEstimator()
cost = cost_func_vqe(x0, ansatz, hamiltonian, estimator)
print(cost)Output:
1.473098768180865
# Estimated usage: < 1 min, benchmarked at 6 seconds on ibm_osaka, 5-23-24
# Load some necessary packages:
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Session, EstimatorV2 as Estimator
# Select the least busy backend:
backend = service.least_busy(
operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
)
# Or get a specific backend:
# backend = service.backend("ibm_brisbane")
# Use a pass manager to transpile the circuit and observable for the specific backend being used:
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_ansatz = pm.run(ansatz)
isa_hamiltonian = hamiltonian.apply_layout(layout=isa_ansatz.layout)
# Set estimator options
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
# Open a Quantum Compute session:
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
cost = cost_func_vqe(x0, isa_ansatz, isa_hamiltonian, estimator)
# Close session after done
session.close()
print(cost)Output:
1.1120776913677988
Revisitaremos esse exemplo em Aplicativos para explorar como aproveitar um otimizador para iterar pelo espaço de pesquisa. De modo geral, isso inclui:
- Aproveitamento de um otimizador para encontrar os parâmetros ideais
- Vinculação de parâmetros ideais ao ansatz para encontrar os valores próprios
- Traduzindo os valores próprios para a definição do nosso problema
Estratégia de medição: velocidade versus precisão
Conforme mencionado, estamos usando um computador quântico ruidoso como um oráculo de caixa preta, onde o ruído pode tornar os valores recuperados não determinísticos, levando a flutuações aleatórias que, por sua vez, prejudicarão - ou até mesmo impedirão completamente - a convergência de determinados otimizadores para uma solução proposta. Esse é um problema geral que precisamos resolver à medida que exploramos cada vez mais a utilidade quântica e progredimos em direção à vantagem quântica:
Podemos utilizar as opções de supressão e mitigação de erros de uma primitiv IBM Quantum e para lidar com o ruído e maximizar a utilidade dos computadores quânticos atuais.
Supressão de erros
A supressão de erros refere-se às técnicas utilizadas para otimizar e transformar um circuito durante a compilação, com o objetivo de minimizar os erros. Essa é uma técnica básica de tratamento de erros que geralmente implica em algum tipo de pré-processamento clássico Custos extras introduzidos por novas técnicas, em relação a uma implementação básica. para o tempo de execução total. A sobrecarga envolve a transpilagem de circuitos para execução em hardware quântico por meio de:
- Expressar o circuito usando as portas nativas disponíveis em um sistema quântico
- Mapeamento dos qubits virtuais para qubits físicos
- Adição de SWAPs com base nos requisitos de conectividade
- Otimização das portas 1Q e 2Q
- Adição de desacoplamento dinâmico a qubits ociosos para evitar os efeitos da decoerência.
As primitivas permitem o uso de técnicas de supressão de erros, definindo a opção optimization_level e selecionando opções avançadas de transpilação. Em um curso posterior, vamos nos aprofundar em diferentes métodos de construção de circuitos para melhorar os resultados, mas, na maioria dos casos, recomendamos definir optimization_level=3.
Visualizaremos o valor do aumento da otimização no processo de transpilação analisando um exemplo de circuito com um comportamento ideal simples.
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
theta = Parameter("theta")
qc = QuantumCircuit(2)
qc.x(1)
qc.h(0)
qc.cp(theta, 0, 1)
qc.h(0)
observables = SparsePauliOp.from_list([("ZZ", 1)])
qc.draw("mpl")Output:
O circuito acima pode produzir valores de expectativa senoidais do observável dado, desde que insiramos fases que abranjam um intervalo apropriado, como .
## Setup phases
import numpy as np
phases = np.linspace(0, 2 * np.pi, 50)
# phases need to be expressed as a list of lists in order to work
individual_phases = [[phase] for phase in phases]Podemos usar um simulador para mostrar a utilidade de uma transpilação otimizada. Voltaremos a usar hardware real para demonstrar a utilidade da atenuação de erros. Usaremos QiskitRuntimeService para obter um backend real (neste caso, ibm_brisbane) e usaremos AerSimulator para simular esse backend, incluindo seu comportamento de ruído.
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_aer import AerSimulator
# get a real backend from the runtime service
service = QiskitRuntimeService()
backend = service.backend("ibm_brisbane")
# generate a simulator that mimics the real quantum system with the latest calibration results
backend_sim = AerSimulator.from_backend(backend)Agora podemos usar um gerenciador de passagens para transpilar o circuito para a “arquitetura de conjunto de instruções” (ISA) do backend. Este é um novo requisito no Serviço de Computação do IBM Quantum : todos os circuitos enviados a um backend devem estar em conformidade com as restrições do alvo do backend, o que significa que devem ser escritos de acordo com a ISA do backend — ou seja, o conjunto de instruções que o dispositivo é capaz de compreender e executar. Essas restrições-alvo são definidas por fatores como os portões de base nativos do dispositivo, sua conectividade de qubits e — quando relevante — suas especificações de temporização de pulsos e outras instruções.
Observe que, no presente caso, faremos isso duas vezes: uma vez com optimization_level = 0 e outra vez com ele definido como 3. Sempre usaremos a primitiva Estimator para estimar os valores de expectativa do observável em diferentes valores de fase.
# Import estimator and specify that we are using the simulated backend:
from qiskit_ibm_runtime import EstimatorV2 as Estimator
estimator = Estimator(mode=backend_sim)
circuit = qc# Use a pass manager to transpile the circuit and observable for the backend being simulated.
# Start with no optimization:
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=0)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
noisy_exp_values = []
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
noisy_exp_values = cost[0]
# Repeat above steps, but now with optimization = 3:
exp_values_with_opt_es = []
pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=3)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
exp_values_with_opt_es = cost[0]Por fim, podemos plotar os resultados e ver que a precisão do cálculo foi razoavelmente boa mesmo sem otimização, mas definitivamente melhorou com o aumento da otimização para o nível 3. Observe que, em circuitos mais profundos e complicados, a diferença entre os níveis de otimização de 0 e 3 provavelmente será mais significativa. Esse é um circuito muito simples usado como um modelo de brinquedo.
import matplotlib.pyplot as plt
plt.plot(phases, noisy_exp_values, "o", label="opt=0")
plt.plot(phases, exp_values_with_opt_es, "o", label="opt=3")
plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label="ideal")
plt.ylabel("Expectation")
plt.legend()
plt.show()Output:
Mitigação de erros
A mitigação de erros refere-se a técnicas que permitem aos usuários reduzir os erros de circuito por meio da modelagem do ruído do dispositivo no momento da execução. Normalmente, isso resulta em uma sobrecarga de pré-processamento quântico relacionada ao treinamento do modelo e em uma sobrecarga de pós-processamento clássico para mitigar erros nos resultados brutos por meio do uso do modelo gerado.
A opção resilience_level da primitiva IBM Quantum especifica o nível de resiliência a ser implementado contra erros. Níveis mais altos geram resultados mais precisos, mas em contrapartida aumentam o tempo de processamento devido à sobrecarga da amostragem quântica. Os níveis de resiliência podem ser usados para definir o equilíbrio entre custo e precisão ao aplicar a mitigação de erros à sua consulta primitiva.
Ao implementar qualquer técnica de mitigação de erros, esperamos que o Um desvio sistemático nas quantidades medidas, geralmente causado por erros. em nossos resultados seja reduzido em relação ao viés anterior, não mitigado. Em alguns casos, o viés pode até desaparecer. No entanto, isso tem um custo. À medida que reduzirmos o viés em nossas quantidades estimadas, a variabilidade estatística aumentará (ou seja, a variância), o que pode ser considerado aumentando ainda mais o número de disparos por circuito em nosso processo de amostragem. Isso introduzirá uma sobrecarga além da necessária para reduzir o viés, portanto, não é feito por padrão. Podemos optar facilmente por esse comportamento ajustando o número de disparos por circuito em options.executions.shots, conforme mostrado no exemplo abaixo.
Neste curso, exploraremos esses modelos de mitigação de erros em um nível geral para ilustrar a mitigação de erros que as primitivas d IBM Quantum m podem realizar, sem a necessidade de entrar em detalhes completos de implementação.
Extinção de erro de leitura giratória (T-REx)
A extinção de erro de leitura girada (T-REx) usa uma técnica conhecida como giro de Pauli para reduzir o ruído introduzido durante o processo de medição quântica. Essa técnica não pressupõe nenhuma forma específica de ruído, o que a torna muito geral e eficaz.
Fluxo de trabalho geral:
- Adquirir dados para o estado zero com inversão aleatória de bits (Pauli X antes da medição)
- Adquirir dados para o estado desejado (ruidoso) com inversão aleatória de bits (Pauli X antes da medição)
- Calcule a função especial para cada conjunto de dados e divida.
Podemos definir isso com options.resilience_level = 1, demonstrado no exemplo abaixo.
Extrapolacão de ruído zero
A extrapolação de ruído zero (ZNE) funciona primeiro amplificando o ruído no circuito que está preparando o estado quântico desejado, obtendo medições para vários níveis diferentes de ruído e usando essas medições para inferir o resultado sem ruído.
Fluxo de trabalho geral:
- Amplificar o ruído do circuito para vários fatores de ruído
- Executar todos os circuitos amplificados por ruído
- Extrapolar de volta para o limite de ruído zero
Podemos definir isso com options.resilience_level = 2. Podemos otimizar ainda mais isso explorando uma variedade de noise_factors, noise_amplifiers, e extrapolators, mas isso está fora do escopo deste curso. Recomendamos que você experimente essas opções conforme descrito aqui.
Cada método vem com sua própria sobrecarga associada: uma compensação entre o número de cálculos quânticos necessários (tempo) e a precisão de nossos resultados:
Utilização das opções do Quantum Compute para mitigação e supressão
Veja a seguir como calcular um valor esperado ao utilizar a mitigação e a supressão de erros no Compute Service d IBM Quantum. Podemos utilizar exatamente o mesmo circuito e a mesma variável observável de antes, mas, desta vez, mantendo o nível de otimização fixo no nível 2 e ajustando a resiliência ou as técnicas de mitigação de erros que estão sendo utilizadas. Esse processo de mitigação de erros ocorre várias vezes ao longo de um ciclo de otimização.
Executamos essa parte em hardware real, pois a atenuação de erros não está disponível em simuladores.
# Estimated usage: 8 minutes, benchmarked on an Eagle processor, 5-23-24
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import (
Session,
EstimatorOptions,
EstimatorV2 as Estimator,
)
# We select the least busy backend
# Select the least busy backend
# backend = service.least_busy(
# operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
# )
# Or use a specific backend
backend = service.backend("ibm_brisbane")
# Initialize some variables to save the results from different runs:
exp_values_with_em0_es = []
exp_values_with_em1_es = []
exp_values_with_em2_es = []
# Use a pass manager to optimize the circuit and observables for the backend chosen:
pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
# Open a session and run with no error mitigation:
estimator_options = EstimatorOptions(resilience_level=0, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em0_es = cost[0]
# Open a session and run with resilience = 1:
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em1_es = cost[0]
# Open a session and run with resilience = 2:
estimator_options = EstimatorOptions(resilience_level=2, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em2_es = cost[0]Como antes, podemos plotar os valores de expectativa resultantes como uma função do ângulo de fase para os três níveis de atenuação de erro usados. Com muita dificuldade, é possível ver que a atenuação de erros melhora um pouco os resultados. Novamente, esse efeito é muito mais pronunciado em circuitos mais profundos e complicados.
import matplotlib.pyplot as plt
plt.plot(phases, exp_values_with_em0_es, "o", label="unmitigated")
plt.plot(phases, exp_values_with_em1_es, "o", label="resil = 1")
plt.plot(phases, exp_values_with_em2_es, "o", label="resil = 2")
plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label="ideal")
plt.ylabel("Expectation")
plt.legend()
plt.show()Output:
Resumo
Com esta lição, você aprendeu a criar uma função de custo:
- Criar uma função de custo
- Como aproveitar os recursos básicos d IBM Quantum s para mitigar e suprimir o ruído
- Como definir uma estratégia de medição para otimizar a velocidade em relação à precisão
Esta é a nossa carga de trabalho variacional de alto nível:
Nossa função de custo é executada durante cada iteração do loop de otimização. A próxima lição explorará como o otimizador clássico usa nossa avaliação da função de custo para selecionar novos parâmetros.
import qiskit
import qiskit_ibm_runtime
print(qiskit.version.get_version_info())
print(qiskit_ibm_runtime.version.get_version_info())Output:
1.1.0
0.23.0