Pular para o conteúdo principal

Corte de fios para estimativa de valores esperados

Estimativa de uso: um minuto em um processador Eagle (NOTA: Isso é apenas uma estimativa. Seu tempo de execução pode variar.)

Contexto

Circuit-knitting é um termo abrangente que engloba vários métodos de particionamento de um circuito em múltiplos subcircuitos menores envolvendo menos portas e/ou qubits. Cada um dos subcircuitos pode ser executado independentemente e o resultado final é obtido por algum pós-processamento clássico sobre o resultado de cada subcircuito. Esta técnica está acessível no addon Qiskit de corte de circuito, uma explicação detalhada da técnica é fornecida na documentação junto com outro material introdutório.

Este notebook trata de um método chamado corte de fios onde o circuito é particionado ao longo do fio [1], [2]. Observe que o particionamento é simples em circuitos clássicos, pois o resultado no ponto de partição pode ser determinado deterministicamente, e é 0 ou 1. No entanto, o estado do qubit no ponto do corte é, em geral, um estado misto. Portanto, cada subcircuito precisa ser medido múltiplas vezes em diferentes bases (geralmente um conjunto tomograficamente completo de bases como a base de Pauli [3], [4] e correspondentemente preparado em seu autoestado. A figura abaixo (cortesia: Tese de PhD, Ritajit Majumdar) mostra um exemplo de corte de fios para um estado GHZ de 4 qubits em três subcircuitos. Aqui MjM_j denota um conjunto de bases (geralmente Pauli X, Y e Z) e PiP_i denota um conjunto de autoestados (geralmente 0|0\rangle, 1|1\rangle, +|+\rangle e +i|+i\rangle).

wc-1.png wc-2.png

Como cada subcircuito tem menos qubits e/ou portas, espera-se que eles sejam menos suscetíveis ao ruído. Este notebook mostra um exemplo onde este método pode ser usado para suprimir efetivamente o ruído no sistema.

Requisitos

Antes de iniciar este tutorial, certifique-se de ter o seguinte instalado:

  • Qiskit SDK v2.0 ou posterior, com suporte de visualização
  • Qiskit Runtime v0.22 ou posterior ( pip install qiskit-ibm-runtime )
  • Addon Qiskit de corte de circuito v0.9.0 ou posterior (pip install qiskit-addon-cutting)

Vamos considerar um circuito de Localização de Muitos Corpos (MBL) para este notebook. O circuito MBL é um circuito eficiente em hardware e é parametrizado por dois parâmetros θ\theta e ϕ\vec{\phi}. Quando θ\theta é definido como 00 e o estado inicial é preparado em 0|0\rangle para todos os qubits, o valor esperado ideal de Zi\langle Z_i \rangle é +1+1 para cada site de qubit ii independentemente dos valores de ϕ\vec{\phi}. Você pode verificar mais detalhes sobre circuitos MBL em este artigo.

Configuração

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-cutting qiskit-ibm-runtime
import numpy as np
import matplotlib.pyplot as plt

from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
from qiskit.quantum_info import PauliList, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.result import sampled_expectation_value

from qiskit_addon_cutting.instructions import CutWire
from qiskit_addon_cutting import (
cut_wires,
expand_observables,
partition_problem,
generate_cutting_experiments,
reconstruct_expectation_values,
)

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2, Batch

class MBLChainCircuit(QuantumCircuit):
def __init__(
self, num_qubits: int, depth: int, use_cut: bool = False
) -> None:
super().__init__(
num_qubits, name=f"MBLChainCircuit<{num_qubits}, {depth}>"
)
evolution = MBLChainEvolution(num_qubits, depth, use_cut)
self.compose(evolution, inplace=True)

class MBLChainEvolution(QuantumCircuit):
def __init__(self, num_qubits: int, depth: int, use_cut) -> None:
super().__init__(
num_qubits, name=f"MBLChainEvolution<{num_qubits}, {depth}>"
)

theta = Parameter("θ")
phis = ParameterVector("φ", num_qubits)

for layer in range(depth):
layer_parity = layer % 2
# print("layer parity", layer_parity)
for qubit in range(layer_parity, num_qubits - 1, 2):
# print(qubit)
self.cz(qubit, qubit + 1)
self.u(theta, 0, np.pi, qubit)
self.u(theta, 0, np.pi, qubit + 1)
if (
use_cut
and layer_parity == 0
and (
qubit == num_qubits // 2 - 1
or qubit == num_qubits // 2
)
):
self.append(CutWire(), [num_qubits // 2])
if use_cut and layer < depth - 1 and layer_parity == 1:
if qubit == num_qubits // 2:
self.append(CutWire(), [qubit])
for qubit in range(num_qubits):
self.p(phis[qubit], qubit)

Parte I. Exemplo em pequena escala

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

Inicialmente construímos um circuito modelo sem nenhum valor de parâmetro específico. Também fornecemos espaços reservados, chamados CutWire, para anotar a posição dos cortes. Para o exemplo em pequena escala, consideramos um circuito MBL de 10 qubits.

num_qubits = 10
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
mbl.draw("mpl", fold=-1)

Output of the previous code cell

Lembre-se de que nosso objetivo é encontrar o valor esperado do observável 1ni=1nZi\frac{1}{n}\sum_{i=1} ^n Z_i quando θ=0\theta=0. Vamos colocar alguns valores aleatórios para o parâmetro ϕ\vec{\phi}.

phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
params
[0,
0.2376615174332788,
0.28244289857682414,
0.019248960591717768,
0.46140600996102477,
0.31408025180068433,
0.718184005135733,
0.991153920182475,
0.09289485768301442,
0.8857848280067783,
0.6177529765767047]

Agora anotamos o circuito para corte inserindo o CutWire adequado para criar dois cortes aproximadamente iguais. Definimos use_cut=True na função e permitimos que ela anote após n2\frac{n}{2} qubits, sendo nn o número de qubits no circuito original.

mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)

Output of the previous code cell

Passo 2: Otimizar problema para execução em hardware quântico

Em seguida, cortamos o circuito em dois subcircuitos menores. Para este exemplo, nos limitamos a apenas 2 subcircuitos. Para isso, usamos o Qiskit Addon: Circuit Cutting.

Cortar o circuito em subcircuitos menores

Cortar o fio em um ponto aumenta a contagem de qubits em um. Além do qubit original, agora existe um qubit extra como espaço reservado para o circuito após o corte. A imagem a seguir fornece uma representação:

wc-4.png

Este Addon usa a função cut_wires para contabilizar os qubits extras decorrentes do corte.

mbl_move = cut_wires(mbl_cut)

Criar e expandir os observáveis

Agora construímos o observável Mz=1ni=1nZiM_z = \frac{1}{n}\sum_{i=1}^n \langle Z_i \rangle. Como o resultado ideal de Zi\langle Z_i \rangle para cada ii é +1+1, o resultado ideal de MzM_z também é +1+1.

observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
observable
PauliList(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII',
'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII',
'IIIIIIIIZI', 'IIIIIIIIIZ'])

No entanto, observe que o número de qubits no circuito aumentou após inserir as operações virtuais Move de 2 qubits após o corte. Portanto, precisamos expandir os observáveis também inserindo identidades para afirmar ao circuito atual.

new_obs = expand_observables(observable, mbl, mbl_move)
new_obs
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])

Observe que cada observável agora se expandiu para acomodar sete qubits, como no circuito com operação Move, em vez dos 6 qubits originais. Em seguida, particionamos o circuito em dois subcircuitos.

partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)

Vamos visualizar os subcircuitos

subcircuits = partitioned_problem.subcircuits
subcircuits[0].draw("mpl", fold=-1)

Output of the previous code cell

subcircuits[1].draw("mpl", fold=-1)

Output of the previous code cell

Os observáveis também foram particionados para se ajustar aos subcircuitos

subobservables = partitioned_problem.subobservables
subobservables
{0: PauliList(['IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IZIIII',
'IIZIII', 'IIIZII', 'IIIIZI', 'IIIIIZ']),
1: PauliList(['ZIIII', 'IZIII', 'IIZII', 'IIIZI', 'IIIIZ', 'IIIII', 'IIIII',
'IIIII', 'IIIII', 'IIIII'])}

Observe que cada subcircuito leva a um número de amostras. A reconstrução leva em conta o resultado de cada uma dessas amostras. Cada uma dessas amostras é denominada subexperiment. Estender o observável usando a operação Move requer uma estrutura de dados PauliList. Também podemos criar o observável MzM_z na estrutura de dados SparsePauliOp mais genérica, que será útil posteriormente durante a reconstrução dos subexperimentos.

M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
M_z
SparsePauliOp(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII', 'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII', 'IIIIIIIIZI', 'IIIIIIIIIZ'],
coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,
0.1+0.j, 0.1+0.j])
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)

Vejamos dois exemplos onde os qubits cortados são medidos em duas bases diferentes. Primeiro, é medido na base Z normal, e depois é medido na base X.

subexperiments[0][6].draw("mpl", fold=-1)

Output of the previous code cell

subexperiments[0][2].draw("mpl", fold=-1)

Output of the previous code cell

Transpilar cada subexperimento

Atualmente precisamos transpilar nossos circuitos antes de submetê-los para execução. Portanto, vamos transpilar cada circuito nos subexperimentos primeiro.

service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)

Agora precisamos transpilar cada um dos circuitos nos subexperimentos. Para isso, primeiro criamos um gerenciador de passos e depois o usamos para transpilar cada um dos circuitos.

pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
isa_subexperiments[0][0].draw("mpl", fold=-1, idle_wires=False)

Output of the previous code cell

Passo 3: Executar usando primitivas Qiskit

Agora vamos executar cada circuito no subexperimento. Qiskit-addon-cutting usa SamplerV2 para executar os subexperimentos.

with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}

Passo 4: Pós-processar e retornar resultado no formato clássico desejado

Uma vez que os circuitos foram executados, agora precisamos recuperar os resultados e reconstruir o valor esperado para o circuito sem corte e o observável original.

# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expval
0.9674376845359803

Verificar cruzadamente

Vamos agora executar o circuito sem corte e verificar o resultado lá. Observe que para execução do circuito sem corte podemos usar diretamente EstimatorV2 para calcular os valores esperados. Mas vamos usar a mesma Primitive por toda parte. Então vamos usar SamplerV2 para obter a distribuição de probabilidade e calcular o valor esperado usando a função sampled_expectation_value.

Primeiro precisamos transpilar o circuito mbl sem corte.

sampler = SamplerV2(mode=backend)

if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)

Em seguida, construímos o pub e executamos o circuito sem corte.

pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
uncut_expval
0.9498046875000001

Observamos que o valor esperado obtido via corte de fios está mais próximo do valor ideal de +1+1 do que aquele sem corte. Vamos agora aumentar a escala do tamanho do problema.

Observamos que o valor esperado obtido via corte de fio é mais próximo do valor ideal de +1+1 do que o não cortado. Vamos agora escalar o tamanho do problema.

Parte II. Aumentando a escala!

Anteriormente, mostramos os resultados para um circuito MBL de 10 qubits. Em seguida, mostramos que a melhoria no valor esperado também é obtida para circuitos maiores. Para mostrar isso, repetimos o processo para um circuito MBL de 60 qubits.

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

num_qubits = 60
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)

Criamos um conjunto aleatório de valores para ϕ\vec{\phi}

phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis

Em seguida, construímos o circuito cortado

mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)

Passo 2: Otimizar problema para execução em hardware quântico

Como mostrado para o exemplo em pequena escala, particionamos o circuito e o observável para os experimentos de corte.

mbl_move = cut_wires(mbl_cut)

# Define observable
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
new_obs = expand_observables(observable, mbl, mbl_move)

# Partition the circuit into subcircuits
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)

# Get subcircuits
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables

Também criamos um objeto SparsePauliOp para o observável com coeficientes apropriados.

M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)

Em seguida, geramos os subexperimentos e transpilamos cada circuito no subexperimento.

subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}

Passo 3: Executar usando primitivas do Qiskit

Usamos o modo Batch para executar todos os circuitos nos subexperimentos.

with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}

Passo 4: Pós-processar e retornar resultado no formato clássico desejado

Vamos agora recuperar os resultados para cada circuito no subexperimento e reconstruir o valor esperado correspondente ao circuito não cortado e ao observável original.

# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expval
0.9631355921427409

Verificação cruzada

Como no exemplo em pequena escala, vamos mais uma vez obter o valor esperado executando o circuito não cortado e comparar o resultado com o corte de circuito. Usaremos o SamplerV2 para manter a uniformidade no uso de Primitivas.

sampler = SamplerV2(mode=backend)

if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)

pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
uncut_expval
0.9426757812499998

Visualizar

Vamos visualizar a melhoria obtida no valor esperado usando corte de fio.

ax = plt.gca()
methods = ["cut", "uncut"]
values = [reconstructed_expval, uncut_expval]

plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.axhline(y=1, color="k", linestyle="--")
ax.set_ylim([0.85, 1.02])
plt.text(0.3, 0.99, "Exact result")
plt.show()

Output of the previous code cell

Inferência

Observamos que tanto nos problemas de pequena quanto de grande escala, o corte de fio leva a um resultado melhor do que o não cortado. Note que nenhuma técnica de mitigação de erro foi usada para esses experimentos. Portanto, a melhoria no resultado que foi obtida é apenas devido ao corte de fio. Pode ser possível melhorar ainda mais os resultados usando diferentes métodos de mitigação juntamente com o corte de circuito.

Além disso, neste notebook, computamos ambos os subcircuitos no mesmo hardware. Em [5], [6], os autores mostram um método para distribuir os subcircuitos em diferentes hardwares usando informações de ruído a fim de maximizar a supressão de ruído e paralelizar o processo.

Apêndice: consideração sobre escala de recursos

O número de circuitos a serem executados aumenta com o número de cortes. Portanto, embora muitos cortes possam produzir subcircuitos pequenos, melhorando ainda mais o desempenho, isso também leva a um número significativamente alto de execuções de circuitos, o que pode não ser prático para a maioria dos casos. Abaixo, mostramos um exemplo do número de subcircuitos correspondente ao número de cortes para um circuito de 50 qubits.

wc-5.png

Note que mesmo para cinco cortes, o número de subexperimentos é em torno de 200k. Portanto, o corte de circuito deve ser usado apenas quando o número de cortes é pequeno.

Um exemplo de circuito amigável a cortes e não amigável a cortes

Circuito amigável a cortes

Como observado anteriormente, um circuito é amigável a cortes quando o circuito pode ser particionado em subcircuitos menores disjuntos com um pequeno número de cortes. Qualquer circuito eficiente em hardware, ou seja, um circuito que requer pouco ou nenhum gate SWAP quando mapeado para o mapa de acoplamento do hardware, é, em geral, amigável a cortes. Abaixo, mostramos um exemplo de um ansatz de preservação de excitação, que é usado em Química Quântica. Note que tal circuito pode ser particionado em dois subcircuitos com um único corte independentemente do número de qubits.

wc-6.png

Circuito não amigável a cortes

Um circuito é não amigável a cortes se, em geral, o número de cortes necessários para formar partições disjuntas cresce significativamente com a profundidade do número de qubits. Lembre-se de que com cada corte um qubit extra é necessário. Assim, com o número de cortes, o número efetivo de qubits também aumenta. Abaixo mostramos um exemplo de um circuito Grover de 3 qubits com uma possível instância de corte.

wc-7.png

Observamos que três cortes são necessários, e o corte é mais vertical do que horizontal. Isso significa que o número de cortes deve escalar linearmente com o número de qubits, o que não é favorável para corte.

Referências

[1] Peng, T., Harrow, A. W., Ozols, M., & Wu, X. (2020). Simulating large quantum circuits on a small quantum computer. Physical review letters, 125(15), 150504.

[2] Tang, W., Tomesh, T., Suchara, M., Larson, J., & Martonosi, M. (2021, April). Cutqc: using small quantum computers for large quantum circuit evaluations. In Proceedings of the 26th ACM International conference on architectural support for programming languages and operating systems (pp. 473-486).

[3] Perlin, M. A., Saleem, Z. H., Suchara, M., & Osborn, J. C. (2021). Quantum circuit cutting with maximum-likelihood tomography. npj Quantum Information, 7(1), 64.

[4] Majumdar, R., & Wood, C. J. (2022). Error mitigated quantum circuit cutting. arXiv preprint arXiv:2211.13431.

[5] Khare, T., Majumdar, R., Sangle, R., Ray, A., Seshadri, P. V., & Simmhan, Y. (2023). Parallelizing Quantum-Classical Workloads: Profiling the Impact of Splitting Techniques. In 2023 IEEE International Conference on Quantum Computing and Engineering (QCE) (Vol. 1, pp. 990-1000). IEEE.

[6] Bhoumik, D., Majumdar, R., Saha, A., & Sur-Kolay, S. (2023). Distributed Scheduling of Quantum Circuits with Noise and Time Optimization. arXiv preprint arXiv:2309.06005.

Pesquisa do tutorial

Por favor, responda a esta breve pesquisa para fornecer feedback sobre este tutorial. Suas percepções nos ajudarão a melhorar nossas ofertas de conteúdo e experiência do usuário.

Link to survey