Skip to main content
IBM Quantum Platform

Experimento em escala utilitária I

Note

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,

H=HZZ+HX=J(i,j)ZiZj+hiXiH = H_{ZZ} + H_X = - J \sum_{(i,j)} Z_i Z_j + h \sum_{i} X_i

em que J>0J > 0 é o acoplamento dos spins do vizinho mais próximo com i<ji < j e hh é 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,

exp(iHZZδt)=(i,j)exp(iJδtZiZj)=(i,j)RZiZj(2Jδt)exp(iHXδt)=iexp(ihδtXi)=iRXi(2hδt)\begin{aligned} \exp(-i H_{ZZ} \delta t) &= \prod_{(i,j)} \exp (i J \delta t Z_i Z_j) = \prod_{(i,j)} \mathrm{R}_{Z_i Z_j} ( - 2 J \delta t) \\ \exp(-i H_X \delta t) &= \prod_{i} \exp (-i h \delta t X_i ) = \prod_{i} \mathrm{R}_{X_i} ( 2 h \delta t) \end{aligned}

em que o tempo de evolução TT é discretizado em T/δtT / \delta t etapas de Trotter e RZiZj(θJ)\mathrm{R}_{Z_i Z_j}(\theta_J) e RXi(θh)\mathrm{R}_{X_i}(\theta_h) são as portas de rotação ZZZZ e XX, respectivamente.

Eles realizaram experimentos em um processador IBM Quantum® Eagle, que é um dispositivo de 127 qubits com conectividade heavy-hex, aplicando interações XX a todos os qubits e interações ZZZZ a todas as bordas do mapa de acoplamento. Observe que todas as interações do ZZZZ 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 θJ=π/2\theta_J=-\pi /2.

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 ×\times de 60 camadas (20 etapas de Trotter) com o Z62\langle Z_{62} \rangle como um observável.

image.png

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 ×\times de 6 camadas (2 etapas de Trotter) com o Z13\langle Z_{13} \rangle 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_aer
import 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 AerSimulator
service = QiskitRuntimeService()

2. Preparação

2.1 Construa RZZ(- π\pi /2)

Primeiro, observe que a porta RZZ em geral requer duas portas CXCX.

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:

Output of the previous code cell

Conforme mencionado acima, para este experimento, nos concentramos na porta RZZ com um ângulo específico, - π\pi / 2. Conforme mostrado no artigo, ele pode ser realizado com apenas uma porta CXCX.

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:

Output of the previous code cell

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:

Output of the previous code cell

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 ZZZZ 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:

Output of the previous code cell

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_map

Os 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:

Output of the previous code cell

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 qc

Observe 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 ×\times 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:

Output of the previous code cell
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 ×\times de 6 camadas (2 etapas de Trotter) com Z13\langle Z_{13} \rangle 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 ×\times 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:

Output of the previous code cell

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 Z62\langle Z_{62} \rangle 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:

Output of the previous code cell

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

Esta página foi útil?
Relate um bug, erro de digitação ou solicite conteúdo no GitHub.