Primeiros passos com corte de circuito usando cortes de fio
Versões dos pacotes
O código nesta página foi desenvolvido usando os seguintes requisitos. Recomendamos usar estas versões ou mais recentes.
qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-aer~=0.17
qiskit-addon-cutting~=0.10.0
Este guia demonstra um exemplo funcional de cortes de fio com o pacote qiskit-addon-cutting. Ele cobre a reconstrução de valores esperados de um circuito de sete qubits usando corte de fio.
Um corte de fio é representado neste pacote como uma instrução de dois qubits Move, que é definida como um reset do segundo qubit sobre o qual a instrução age, seguido de uma troca de ambos os qubits. Essa operação é equivalente a transferir o estado do primeiro qubit para o segundo qubit, enquanto simultaneamente descarta o estado de entrada do segundo qubit.
O pacote é projetado para ser consistente com a forma como você deve tratar os cortes de fio ao agir sobre qubits físicos. Por exemplo, um corte de fio pode pegar o estado do qubit físico e continuá-lo como um qubit físico após o corte. Você pode pensar no "corte de instrução" como um framework unificado para considerar tanto cortes de fio quanto cortes de gate dentro do mesmo formalismo (já que um corte de fio é apenas uma instrução Move cortada). Usar esse framework para corte de fio também permite a reutilização de qubits, o que é explicado na seção sobre cortar fios manualmente.
A instrução de qubit único CutWire funciona como uma interface mais abstrata e simples para trabalhar com cortes de fio. Ela permite que você indique onde no circuito um fio deve ser cortado em alto nível e que o addon de corte de circuito insira as instruções Move apropriadas para você.
O exemplo a seguir demonstra a reconstrução do valor esperado após o corte de fio. Você criará um circuito com vários gates não locais e definirá observáveis para estimar.
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-aer qiskit-ibm-runtime
import numpy as np
from qiskit import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
from qiskit_ibm_runtime import SamplerV2, Batch
from qiskit_aer.primitives import EstimatorV2
from qiskit_addon_cutting.instructions import Move, CutWire
from qiskit_addon_cutting import (
partition_problem,
generate_cutting_experiments,
cut_wires,
expand_observables,
reconstruct_expectation_values,
)
qc_0 = QuantumCircuit(7)
for i in range(7):
qc_0.rx(np.pi / 4, i)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)
qc_0.cx(3, 4)
qc_0.cx(3, 5)
qc_0.cx(3, 6)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)
# Define observable
observable = SparsePauliOp(["ZIIIIII", "IIIZIII", "IIIIIIZ"])
# Draw circuit
qc_0.draw("mpl")
Cortar fios usando a instrução de alto nível CutWire
Em seguida, faça cortes de fio usando a instrução de qubit único CutWire no qubit . Assim que os subexperimentos estiverem preparados para execução, use a função cut_wires() para transformar as instruções CutWire em instruções Move em qubits recém-alocados.
qc_1 = QuantumCircuit(7)
for i in range(7):
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(CutWire(), [3])
qc_1.cx(3, 4)
qc_1.cx(3, 5)
qc_1.cx(3, 6)
qc_1.append(CutWire(), [3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.draw("mpl")
Quando um circuito é expandido por meio de um ou mais cortes de fio, o observável precisa ser atualizado para levar em conta os qubits extras que são introduzidos. O pacote qiskit-addon-cutting possui uma função utilitária expand_observables(), que recebe objetos PauliList e os circuitos original e expandido como argumentos, e retorna um novo PauliList.
Este PauliList retornado não conterá nenhuma informação sobre os coeficientes do observável original, mas estes podem ser ignorados até a reconstrução do valor esperado final.
# Transform CutWire instructions to Move instructions
qc_2 = cut_wires(qc_1)
# Expand the observable to match the new circuit size
expanded_observable = expand_observables(observable.paulis, qc_0, qc_2)
print(f"Expanded Observable: {expanded_observable}")
qc_2.draw("mpl")
Expanded Observable: ['ZIIIIIIII', 'IIIZIIIII', 'IIIIIIIIZ']
Particionar o circuito e o observável
Agora o problema pode ser separado em partições. Isso é feito usando a função partition_problem() com um conjunto opcional de rótulos de partição para especificar como separar o circuito. Qubits que compartilham um rótulo de partição comum são agrupados, e quaisquer gates não locais que abrangem mais de uma partição são cortados.
Se nenhum rótulo de partição for fornecido, o particionamento será determinado automaticamente com base na conectividade do circuito. Leia a próxima seção sobre cortar fios manualmente para mais informações sobre como incluir rótulos de partição.
partitioned_problem = partition_problem(
circuit=qc_2,
observables=expanded_observable,
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases
print(f"Subobservables to measure: \n{subobservables}\n")
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
subcircuits[0].draw("mpl")
Subobservables to measure:
{0: PauliList(['IIIII', 'ZIIII', 'IIIIZ']), 1: PauliList(['ZIII', 'IIII', 'IIII'])}
Sampling overhead: 256.0
subcircuits[1].draw("mpl")
Neste esquema de particionamento, você cortou dois fios, resultando em um overhead de amostragem de .
Gerar subexperimentos para executar e pós-processar resultados
Para estimar o valor esperado do circuito completo, vários subexperimentos são gerados a partir da distribuição de quasi-probabilidade conjunta dos gates decompostos e depois executados em um (ou mais) QPUs. O método generate_cutting_experiments faz isso recebendo argumentos para os dicionários subcircuits e subobservables que você criou acima, bem como para o número de amostras a serem extraídas da distribuição.
O argumento num_samples especifica quantas amostras extrair da distribuição de quasi-probabilidade e determina a precisão dos coeficientes usados para a reconstrução. Passar infinito (np.inf) garante que todos os coeficientes sejam calculados exatamente. Leia a documentação da API sobre geração de pesos e geração de experimentos de corte para mais informações.
# Generate subexperiments
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits, observables=subobservables, num_samples=np.inf
)
# Set a backend to use and transpile the subexperiments
backend = FakeManilaV2()
pass_manager = generate_preset_pass_manager(
optimization_level=1, backend=backend
)
isa_subexperiments = {
label: pass_manager.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
# Submit each partition's subexperiments to the Qiskit Runtime Sampler
# primitive, in a single batch so that the jobs will run back-to-back.
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()
}
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
Por fim, o valor esperado do circuito completo pode ser reconstruído usando o método reconstruct_expectation_values().
O bloco de código abaixo reconstrói os resultados e os compara com o valor esperado exato.
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
# Apply the coefficients of the original observable
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
# Compute the exact expectation value using the `qiskit_aer` package.
estimator = EstimatorV2()
exact_expval = estimator.run([(qc_0, observable)]).result()[0].data.evs
print(
f"Reconstructed expectation value: {np.real(np.round(reconstructed_expval, 8))}"
)
print(f"Exact expectation value: {np.round(exact_expval, 8)}")
print(
f"Error in estimation: {np.real(np.round(reconstructed_expval-exact_expval, 8))}"
)
print(
f"Relative error in estimation: {np.real(np.round((reconstructed_expval-exact_expval) / exact_expval, 8))}"
)
Reconstructed expectation value: 1.45965266
Exact expectation value: 1.59099026
Error in estimation: -0.1313376
Relative error in estimation: -0.08255085
Para reconstruir com precisão o valor esperado, os coeficientes do observável original (que são diferentes da saída de generate_cutting_experiments()) devem ser aplicados à saída da reconstrução, uma vez que essa informação é perdida quando os experimentos de corte são gerados ou quando o observável é expandido.
Tipicamente, esses coeficientes podem ser aplicados por meio de numpy.dot() conforme mostrado anteriormente.
Cortar fios usando a instrução de baixo nível Move
Uma limitação de usar a instrução CutWire de nível mais alto é que ela não permite a reutilização de qubits. Se isso for desejado em um experimento de corte, você pode em vez disso inserir manualmente instruções Move. Porém, como a instrução Move descarta o estado do qubit de destino, é importante que esse qubit não compartilhe nenhum emaranhamento com o restante do sistema. Caso contrário, a operação de reset fará com que o estado do circuito colapse parcialmente após o corte de fio.
O bloco de código abaixo realiza um corte de fio no qubit para o mesmo circuito de exemplo mostrado anteriormente. A diferença aqui é que você pode reutilizar um qubit invertendo a operação Move onde o segundo corte de fio foi feito (no entanto, isso nem sempre é possível e depende do circuito sendo cortado).
qc_1 = QuantumCircuit(8)
for i in [*range(4), *range(5, 8)]:
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(Move(), [3, 4])
qc_1.cx(4, 5)
qc_1.cx(4, 6)
qc_1.cx(4, 7)
qc_1.append(Move(), [4, 3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
# Expand observable
observable_expanded = SparsePauliOp(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"])
qc_1.draw("mpl")
O circuito acima agora pode ser particionado e os experimentos de corte gerados. Para especificar explicitamente como o circuito deve ser particionado, você pode adicionar rótulos de partição à função partition_problem(). Qubits que compartilham um rótulo de partição comum são agrupados, e quaisquer gates não locais que abrangem mais de uma partição são cortados. As chaves do dicionário gerado por partition_problem() corresponderão às especificadas na string de rótulo.
partitioned_problem = partition_problem(
circuit=qc_1,
partition_labels="AAAABBBB",
observables=observable_expanded.paulis,
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases
print(f"Subobservables to measure: \n{subobservables}\n")
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
subcircuits["A"].draw("mpl")
Subobservables to measure:
{'A': PauliList(['IIII', 'ZIII', 'IIIZ']), 'B': PauliList(['ZIII', 'IIII', 'IIII'])}
Sampling overhead: 256.0
subcircuits["B"].draw("mpl")
Agora os experimentos de corte podem ser gerados e o valor esperado reconstruído da mesma forma que na seção anterior.
Próximos passos
- Leia o guia Primeiros passos com corte de circuito usando cortes de gate.
- Leia o artigo no arXiv sobre corte de fio ótimo para entender melhor a equivalência entre corte de fio e corte de gate.