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 denota um conjunto de bases (geralmente Pauli X, Y e Z) e denota um conjunto de autoestados (geralmente , , e ).
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 e . Quando é definido como e o estado inicial é preparado em para todos os qubits, o valor esperado ideal de é para cada site de qubit independentemente dos valores de . 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)
Lembre-se de que nosso objetivo é encontrar o valor esperado do observável quando . Vamos colocar alguns valores aleatórios para o parâmetro .
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 qubits, sendo 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)
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:
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 . Como o resultado ideal de para cada é , o resultado ideal de também é .
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)
subcircuits[1].draw("mpl", fold=-1)
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 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)
subexperiments[0][2].draw("mpl", fold=-1)
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)
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 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 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
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()
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.
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.

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.
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.