Experimento em escala utilitária I
Tamiya Onodera (5 de julho de 2024)
Baixe o pdf da palestra original. Observe que alguns trechos de código podem se tornar obsoletos, pois são imagens estáticas.
O tempo aproximado da QPU para executar esse experimento é de 45 segundos.
1. Introdução ao documento de utilidade pública
Nesta lição, executamos um circuito em escala de utilidade que aparece no que chamamos informalmente de "o documento de utilidade" publicado na Nature Vol. 618, 15 de junho de 2023. O artigo trata da evolução temporal do modelo de Ising de campo transversal 2D. Em particular, eles consideram a dinâmica de tempo do Hamiltoniano,
em que é o acoplamento dos spins do vizinho mais próximo com e é o campo transversal global.
Eles simulam a dinâmica do spin a partir de um estado inicial por meio da decomposição de Trotter de primeira ordem do operador de evolução temporal,
em que o tempo de evolução é discretizado em etapas de Trotter e e são as portas de rotação e , respectivamente.
Eles realizaram experimentos em um processador IBM Quantum® Eagle, que é um dispositivo de 127 qubits com conectividade heavy-hex, aplicando interações a todos os qubits e interações a todas as bordas do mapa de acoplamento. Observe que todas as interações do não podem ser aplicadas simultaneamente devido à "dependência de dados". Por isso, eles colorem o mapa de acoplamento para agrupá-los em camadas. Os que estão em uma camada recebem a mesma cor e podem ser aplicados em paralelo.
Além disso, para fins de simplicidade experimental, eles se concentraram no caso .
A nova contribuição do artigo é que eles construíram circuitos quânticos em uma escala além da simulação de vetor de estado, executaram-nos em computadores quânticos com ruído e conseguiram extrair resultados confiáveis. Ou seja, eles demonstraram a utilidade dos computadores quânticos barulhentos. Ao fazer isso, eles aplicaram a extrapolação de ruído zero (ZNE) com amplificação de erro probabilístico (PEA) para atenuar os erros de dispositivos ruidosos.
A partir de então, passamos a chamar esses experimentos e circuitos de "escala de utilidade".
1.1 Seu objetivo
Seu objetivo nesta lição é construir um circuito de escala de utilidade e executá-lo em um processador Eagle. Está além do escopo deste notebook extrair resultados confiáveis, em parte porque a AEP é um recurso experimental do Qiskit no momento em que escrevemos e em parte porque a aplicação do ZNE com AEP levará um bom tempo.
Em termos concretos, pedimos que você construa e execute o circuito correspondente à Figura 4b do documento e trace os pontos "não mitigados" por conta própria. Como você pode ver, é um circuito de 127 qubits de 60 camadas (20 etapas de Trotter) com o como um observável.
Parece muito importante? Não se preocupe. As três últimas lições deste curso fornecem os passos a serem seguidos. Para começar, demonstraremos um experimento de menor escala que consiste em construir e executar em um dispositivo falso um circuito de 27 qubits de 6 camadas (2 etapas de Trotter) com o como observável.
Isso é tudo para a introdução. Vamos embarcar em uma aventura em escala de serviços públicos!
import qiskit
qiskit.__version__Output:
'2.0.2'
#!pip install qiskit_ibm_runtime
#!pip install qiskit_aerimport matplotlib.pyplot as plt
import numpy as np
import rustworkx as rx
from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Parameter
from qiskit.circuit.library import YGate
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime import (
QiskitRuntimeService,
fake_provider,
EstimatorV2 as Estimator,
)
from qiskit_aer import AerSimulatorservice = QiskitRuntimeService()2. Preparação
2.1 Construa RZZ(- /2)
Primeiro, observe que a porta RZZ em geral requer duas portas .
from qiskit.circuit.library import RZZGate
θ_h = Parameter("$\\theta_h$")
qc1 = QuantumCircuit(2)
qc1.append(RZZGate(θ_h), [0, 1])
qc1.decompose(reps=1).draw("mpl")Output:
Conforme mencionado acima, para este experimento, nos concentramos na porta RZZ com um ângulo específico, - / 2. Conforme mostrado no artigo, ele pode ser realizado com apenas uma porta .
qc2 = QuantumCircuit(2)
qc2.sdg([0, 1])
qc2.append(YGate().power(1 / 2), [1])
qc2.cx(0, 1)
qc2.append(YGate().power(1 / 2).adjoint(), [1])
qc2.draw("mpl")Output:
Para referência futura, definimos uma porta em termos desse circuito.
rzz = qc2.to_gate(label="RZZ")Vamos fazer um uso aleatório do recém-definido rzz.
qc3 = QuantumCircuit(3)
qc3.append(rzz, [0, 1])
qc3.append(rzz, [0, 2])
display(qc3.draw("mpl"))
# display(qc.decompose(reps=1).draw("mpl"))Output:
Antes de continuar usando isso, vamos verificar a equivalência lógica de qc1 (a porta RZZ) para -pi/2 e nossa porta recém-definida rzz ou qc2 :
from qiskit.quantum_info import Operator
op1 = Operator(qc1.assign_parameters([-np.pi / 2]))
op2 = Operator(qc2)
op1.equiv(op2)Output:
True
2.2 Colorir o mapa de acoplamento
Vamos estudar como colorir o mapa de acoplamento de um backend. Isso é necessário para agrupar as interações do em camadas.
Para começar, vamos visualizar o mapa de acoplamento de um backend. Observe que os mapas de acoplamento são hexagonais pesados para todos os dispositivos IBM Quantum atuais.
backend = service.least_busy(operational=True, simulator=False)
backend.coupling_map.draw()Output:
Para colorir um mapa de acoplamento, usamos o rustworkx, que é um pacote do Python para trabalhar com gráficos e redes complexas. Ele fornece vários algoritmos de coloração, que são todos heurísticos e, portanto, não têm a garantia de encontrar uma coloração mínima.
Dito isso, como os grafos heavy-hex são bipartidos, pegamos o graph_bipartite_edge_color, que deve encontrar uma coloração mínima para esses grafos.
def color_coupling_map(backend):
graph = backend.coupling_map.graph
undirected_graph = graph.to_undirected(multigraph=False)
edge_color_map = rx.graph_bipartite_edge_color(undirected_graph)
if edge_color_map is None:
edge_color_map = rx.graph_greedy_edge_color(undirected_graph)
# build a map from color to a list of edges
edge_index_map = undirected_graph.edge_index_map()
color_edges_map = {color: [] for color in edge_color_map.values()}
for edge_index, color in edge_color_map.items():
color_edges_map[color].append(
(edge_index_map[edge_index][0], edge_index_map[edge_index][1])
)
return edge_color_map, color_edges_mapOs gráficos hexagonais pesados devem ser pintados em três cores. Vamos verificar isso para o mapa de acoplamento acima.
edge_color_map, color_edges_map = color_coupling_map(backend)
print(
f"{backend.name}, {backend.num_qubits}-qubit device, {len(color_edges_map.keys())} colors assigned."
)Output:
ibm_strasbourg, 127-qubit device, 3 colors assigned.
Sim, é verdade!
Por diversão, vamos pintar o mapa de acoplamento com a coloração obtida, usando o recurso de visualização do rustworks.
color_str_map = {0: "green", 1: "red", 2: "blue"}
undirected_graph = backend.coupling_map.graph.to_undirected(multigraph=False)
for i in undirected_graph.edge_indices():
undirected_graph.get_edge_data_by_index(i)["color"] = color_str_map[
edge_color_map[i]
]
rx.visualization.graphviz_draw(
undirected_graph, method="neato", edge_attr_fn=lambda edge: {"color": edge["color"]}
)Output:
3. Resolva a evolução temporal trotterizada de um modelo Ising e 2D.
Vamos definir uma rotina para construir um circuito do papel de utilidade para a evolução temporal de um modelo 2D Ising. A rotina recebe três parâmetros: um backend, um número inteiro que indica o número de etapas do Trotter e um booleano que controla a inserção da barreira.
def get_utility_circuit(backend, num_steps: int, barrier: bool = False):
num_qubits = backend.num_qubits
_, color_edges_map = color_coupling_map(backend)
θ_h = Parameter("$\\theta_h$")
qc = QuantumCircuit(num_qubits)
for i in range(num_steps):
qc.rx(θ_h, range(num_qubits))
for _, edge_list in color_edges_map.items():
for edge in edge_list:
qc.append(rzz, edge)
if barrier:
qc.barrier()
return qcObserve que já realizamos manualmente o mapeamento e o roteamento de qubits para o circuito construído. Assim, quando transpilarmos o circuito posteriormente, não pediremos (não devemos ) que o transpilador faça o mapeamento e o roteamento dos qubits. Como você verá em breve, nós o invocamos com o nível de otimização como 1 e o método de layout como "trivial".
Em seguida, definimos uma rotina simples para obter as informações sobre o circuito construído para uma verificação rápida.
def get_circuit_info(qc: QuantumCircuit, reps: int = 0):
qc0 = qc.decompose(reps=reps)
return (
f"{qc0.num_qubits} qubits × {qc0.depth(lambda x: x.operation.num_qubits == 2)} layers ({qc0.depth()}-depth)"
+ ", "
+ f"""Gate breakdown: {", ".join([f"{k.upper()} {v}" for k, v in qc0.count_ops().items()])}"""
)Vamos exercitar essas rotinas. Você deverá ver um circuito de 27 qubits 15 camadas (5 etapas de Trotter). Como o dispositivo falso tem 28 bordas, deve haver 28*5 medidores de emaranhamento.
backend = fake_provider.FakeTorontoV2()
num_steps = 5
qc = get_utility_circuit(backend, num_steps, True)
display(qc.draw(output="mpl", fold=-1))
print(get_circuit_info(qc, reps=0))
print(get_circuit_info(qc, reps=1))Output:
27 qubits × 15 layers (20-depth), Gate breakdown: CIRCUIT-165 140, RX 135, BARRIER 5
27 qubits × 15 layers (60-depth), Gate breakdown: SDG 280, UNITARY 280, CX 140, R 135, BARRIER 5
4. Resolva a versão de 27 qubits do problema.
Agora demonstramos uma versão em menor escala do experimento de utilidade. Criamos um circuito de 27 qubit de 6 camadas (2 etapas de Trotter) com como observável e o executamos em AerSimulator e em um dispositivo falso.
Obviamente, seguimos nosso fluxo de trabalho de quatro etapas, o "padrão Qiskit", que consiste em Mapear, Otimizar, Executar e Pós-processar. De forma mais concreta,
- Mapear entradas clássicas para uma computação quântica.
- Otimizar circuitos para computação quântica.
- Executar circuitos usando primitivos.
- Pós-processamento e retorno dos resultados em formato clássico.
A seguir, temos a etapa do mapa para criar um circuito para um experimento de menor escala. Temos então um conjunto de Optimize and Execute para AerSimulator e outro para um dispositivo falso. Por fim, temos a etapa Post-Process para plotar os resultados.
4.1 Passo 1: Mapa
backend = fake_provider.FakeTorontoV2() # a 27 qubit fake device.
num_steps = 2
qc = get_utility_circuit(backend, num_steps)
obs = SparsePauliOp.from_sparse_list(
[("Z", [13], 1)], num_qubits=backend.num_qubits
) # Falcon
angles = [
0,
0.1,
0.2,
0.3,
0.4,
0.5,
0.6,
0.7,
0.8,
1.0,
np.pi / 2,
] # We try 11 angles for theta_h.4.2 Etapas 2 e 3: Otimizar e Executar (Simulador)
backend_sim = AerSimulator()
transpiled_qc_sim = transpile(
qc, backend_sim, optimization_level=1, layout_method="trivial"
)
transpiled_obs_sim = obs.apply_layout(layout=transpiled_qc_sim.layout)
print(get_circuit_info(qc, reps=1))
print(get_circuit_info(transpiled_qc_sim, reps=1))Output:
27 qubits × 6 layers (23-depth), Gate breakdown: SDG 112, UNITARY 112, CX 56, R 54
27 qubits × 6 layers (16-depth), Gate breakdown: U3 80, CX 56, R 54, U1 32, U 28
Um usuário executou a próxima célula usando um MacBook Pro com 2.3 GHz quad-core Intel Core i7 Processor equipado com 32GB 3LPDDR4X RAM, que está executando macOS 14.5. O site 161ms foi levado em tempo de parede. Cada laptop será um pouco diferente.
%%time
params = [[p] for p in angles]
estimator = Estimator(mode=backend_sim)
pub = (transpiled_qc_sim, transpiled_obs_sim, params)
result_sim = estimator.run([pub]).result()Output:
CPU times: user 231 ms, sys: 186 ms, total: 417 ms
Wall time: 111 ms
4.3 Etapas 2 e 3: Otimizar e executar (dispositivo falso)
backend_fake = fake_provider.FakeTorontoV2()
transpiled_qc_fake = transpile(
qc, backend_fake, optimization_level=1, layout_method="trivial"
)
transpiled_obs_fake = obs.apply_layout(layout=transpiled_qc_fake.layout)
print(get_circuit_info(qc, reps=1))
print(get_circuit_info(transpiled_qc_fake, reps=1))Output:
27 qubits × 6 layers (23-depth), Gate breakdown: SDG 112, UNITARY 112, CX 56, R 54
27 qubits × 6 layers (49-depth), Gate breakdown: SDG 324, U1 274, H 162, CX 56, U3 14
Quando o mesmo usuário executou a próxima célula com o mesmo ambiente acima, foi necessário 2min 19s em Wall Time. A execução de um circuito em um dispositivo falso invoca a simulação com ruído, que leva muito mais tempo do que a simulação exata. Recomendamos que você não execute um circuito maior (como um circuito de 27 qubits 9 camadas com 3 etapas de Trotter) em um dispositivo falso.
%%time
params = [[p] for p in angles]
estimator = Estimator(mode=backend_fake)
pub = (transpiled_qc_fake, transpiled_obs_fake, params)
result_fake = estimator.run([pub]).result()Output:
CPU times: user 4min 42s, sys: 9.35 s, total: 4min 51s
Wall time: 38.3 s
4.4 Etapa 4: Pós-processamento
Apresentamos os resultados de simulações exatas e com ruído. Você vê os graves efeitos do ruído em FakeToronto.
plt.plot(angles, result_fake[0].data.evs, "o", label="Fake Device")
plt.plot(angles, result_sim[0].data.evs, "o", label="AerSimulator")
plt.xlabel("$\\mathrm{R_x}$ angle $\\theta_h$")
plt.title("$\\langle Z_{13} \\rangle$")
plt.legend()
plt.show()Output:
5. Resolva a versão de 127 qubits do problema
Seu objetivo é executar o experimento em escala de utilidade, conforme mencionado no início. Você criará e executará um circuito de 127 qubits e 60 camadas (20 etapas de Trotter) com como um observável. Recomendamos que você tente fazer isso por conta própria, usando o código para a versão de 27 qubit quando apropriado. Mas a solução é fornecida aqui.
Solução:
5.1 Passo 1: Mapa
# backend_map = service.backend("ibm_brisbane")
backend_map = service.least_busy(operational=True, simulator=False)
num_steps = 20
qc = get_utility_circuit(backend_map, num_steps)
obs = SparsePauliOp.from_sparse_list(
[("Z", [62], 1)], num_qubits=backend_map.num_qubits
) # Eagle
angles = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 1.0, np.pi / 2]5.2 Etapas 2 e 3: Otimizar e executar
Observamos que o mapa de acoplamento do processador Eagle tem 144 bordas.
# backend = service.backend("ibm_brisbane")
backend = backend_map
transpiled_qc = transpile(qc, backend, optimization_level=1, layout_method="trivial")
transpiled_obs = obs.apply_layout(layout=transpiled_qc.layout)
print(get_circuit_info(qc, reps=1))
print(get_circuit_info(transpiled_qc))Output:
156 qubits × 60 layers (221-depth), Gate breakdown: SDG 7040, UNITARY 7040, CX 3520, R 3120
156 qubits × 60 layers (201-depth), Gate breakdown: RZ 11933, SX 6240, CZ 3520
params = [[p] for p in angles]
estimator = Estimator(mode=backend)
pub = (transpiled_qc, transpiled_obs, params)
job = estimator.run([pub])
job_id = job.job_id()
print(f"job id={job_id}")Output:
job id=d1479n6qf56g0081sxa0
5.3 Pós-processamento
Fornecemos os valores para os pontos "atenuados" na Figura 4b do documento de utilidade. Faça um gráfico com seus resultados.
result_paper = [
1.0171,
1.0044,
0.9563,
0.9602,
0.8394,
0.8120,
0.5466,
0.4556,
0.1953,
0.0141,
0.0117,
]
# REPLACE WITH YOUR OWN JOB ID
job = service.job(job_id)
plt.plot(angles, job.result()[0].data.evs, "o", label=f"{job.backend().name}")
plt.plot(angles, result_paper, "o", label="Utility Paper")
plt.xlabel("$\\mathrm{R_x}$ angle $\\theta_h$")
plt.title("$\\langle Z_{62} \\rangle$")
plt.legend()
plt.show()Output:
Seus resultados são semelhantes aos "não mitigados" na Figura 4b? Eles podem ser muito diferentes, dependendo do dispositivo e de sua condição no momento do experimento. Não se preocupe com os resultados em si. O que verificaremos é se você fez a codificação corretamente. Se você fez isso, parabéns, você chegou à linha de partida da era dos serviços públicos.
Assim como no documento Utility, os cientistas de todo o mundo têm se esforçado muito para extrair resultados significativos mesmo na presença de ruído. O objetivo final desse esforço coletivo é a vantagem quântica: um estado no qual os computadores quânticos podem resolver alguns problemas de uso no setor mais rapidamente, com maior fidelidade ou de forma mais barata do que os computadores clássicos. Não é provável que isso seja um evento único, mas sim uma era em que a reprodução clássica de resultados quânticos leva cada vez mais tempo, até que, em algum momento, o tempo de espera quântico se torna extremamente importante. Uma coisa é clara sobre a vantagem quântica: só chegamos lá por meio de experimentos em escala de utilidade. Se esse curso fizer com que você participe da busca, que é cheia de desafios e diversão, ficaremos muito felizes.
Referência
- Kim, Y., Eddins, A., Anand, S. et al. Evidência da utilidade da computação quântica antes da tolerância a falhas. Nature 618, 500-505 (2023). https://doi.org/10.1038/s41586-023-06096-3