Comece a usar o corte de circuitos com cortes de gates
Versões dos pacotes
O código nesta página foi desenvolvido com 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 dois exemplos funcionais de cortes de gates com o pacote qiskit-addon-cutting. O primeiro exemplo mostra como reduzir a profundidade do circuito (o número de instruções do circuito) cortando gates de entrelaçamento em qubits não adjacentes que, de outra forma, incorreriam em um overhead de SWAP ao serem transpilados para o hardware. O segundo exemplo aborda como usar o corte de gates para reduzir a largura do circuito (o número de qubits), dividindo um circuito em vários circuitos com menos qubits.
Ambos os exemplos usarão o ansatz efficient_su2 e reconstruirão o mesmo observável.
Corte de gates para reduzir a profundidade do circuito
O fluxo de trabalho a seguir reduz a profundidade de um circuito cortando gates distantes, evitando uma grande série de gates SWAP que seriam introduzidos de outra forma.
Comece com o ansatz efficient_su2, com entrelaçamento "circular" para introduzir gates distantes.
# 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.circuit.library import efficient_su2
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager
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 import (
cut_gates,
partition_problem,
generate_cutting_experiments,
reconstruct_expectation_values,
)
circuit = efficient_su2(num_qubits=4, entanglement="circular")
circuit.assign_parameters([0.4] * len(circuit.parameters), inplace=True)
observable = SparsePauliOp(["ZZII", "IZZI", "-IIZZ", "XIXI", "ZIZZ", "IXIX"])
print(f"Observable: {observable}")
circuit.draw("mpl", scale=0.8)
Observable: SparsePauliOp(['ZZII', 'IZZI', 'IIZZ', 'XIXI', 'ZIZZ', 'IXIX'],
coeffs=[ 1.+0.j, 1.+0.j, -1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])
Cada um dos gates CNOT entre os qubits e introduz dois gates SWAP após a transpilação (assumindo que os qubits estão conectados em linha reta). Para evitar esse aumento de profundidade, você pode substituir esses gates distantes por objetos TwoQubitQPDGate usando o método cut_gates(). Essa função também retorna uma lista de instâncias QPDBasis — uma para cada decomposição.
# Find the indices of the distant gates
cut_indices = [
i
for i, instruction in enumerate(circuit.data)
if {circuit.find_bit(q)[0] for q in instruction.qubits} == {0, 3}
]
# Decompose distant CNOTs into TwoQubitQPDGate instances
qpd_circuit, bases = cut_gates(circuit, cut_indices)
qpd_circuit.draw("mpl", scale=0.8)
Agora que as instruções de corte de gate foram adicionadas, os subexperimentos terão uma profundidade menor após a transpilação do que o circuito original. O trecho de código abaixo gera os subexperimentos usando generate_cutting_experiments, que recebe o circuito e o observável a ser reconstruído.
O argumento num_samples especifica quantas amostras devem ser extraídas da distribuição de quase-probabilidade e determina a precisão dos coeficientes usados na reconstrução. Passar infinito (np.inf) garantirá 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.
Depois que os subexperimentos forem gerados, você pode transpilá-los e usar a primitiva Sampler para amostrar a distribuição e reconstruir os valores esperados estimados. O bloco de código a seguir gera, transpila e executa os subexperimentos. Em seguida, reconstrói os resultados e os compara com o valor esperado exato.
# Generate the subexperiments and sampling coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=qpd_circuit, observables=observable.paulis, 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 = pass_manager.run(subexperiments)
# Set up the Qiskit Runtime Sampler primitive, submit the subexperiments, and retrieve the results
sampler = SamplerV2(backend)
job = sampler.run(isa_subexperiments, shots=4096 * 3)
results = job.result()
# Reconstruct the results
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
observable.paulis,
)
# Apply the coefficients of the original observable
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
estimator = EstimatorV2()
exact_expval = (
estimator.run([(circuit, observable, [0.4] * len(circuit.parameters))])
.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: 0.49812826
Exact expectation value: 0.50497603
Error in estimation: -0.00684778
Relative error in estimation: -0.0135606
Para reconstruir com precisão o valor esperado, os coeficientes do observável original (que são diferentes dos coeficientes na saída de generate_cutting_experiments()) devem ser aplicados à saída da reconstrução, pois essa informação é perdida quando os experimentos de corte são gerados ou quando o observável é expandido.
Normalmente, esses coeficientes podem ser aplicados por meio de numpy.dot() conforme mostrado acima.
Corte de gates para reduzir a largura do circuito
Esta seção demonstra o uso do corte de gates para reduzir a largura do circuito. Comece com o mesmo efficient_su2, mas use o entrelaçamento "linear".
qc = efficient_su2(4, entanglement="linear", reps=2)
qc.assign_parameters([0.4] * len(qc.parameters), inplace=True)
observable = SparsePauliOp(["ZZII", "IZZI", "-IIZZ", "XIXI", "ZIZZ", "IXIX"])
print(f"Observable: {observable}")
qc.draw("mpl", scale=0.8)
Observable: SparsePauliOp(['ZZII', 'IZZI', 'IIZZ', 'XIXI', 'ZIZZ', 'IXIX'],
coeffs=[ 1.+0.j, 1.+0.j, -1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])
Em seguida, gere os subcircuitos e os subobserváveis que você executará usando a função partition_problem(). Essa função recebe o circuito, o observável e um esquema de particionamento opcional, e retorna os circuitos e observáveis cortados na forma de um dicionário.
O particionamento é definido por uma string de rótulo no formato "AABB", onde cada rótulo nessa string corresponde ao qubit no mesmo índice do argumento circuit. Qubits que compartilham um rótulo de partição comum são agrupados, e quaisquer gates não locais que abranjam mais de uma partição serão cortados.
O argumento observables em partition_problem é do tipo PauliList. Os coeficientes e fases dos termos do observável são ignorados durante a decomposição do problema e a execução dos subexperimentos. Eles podem ser reaplicados durante a reconstrução do valor esperado.
partitioned_problem = partition_problem(
circuit=qc, partition_labels="AABB", observables=observable.paulis
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
print(f"Subobservables: {subobservables}")
subcircuits["A"].draw("mpl", scale=0.8)
Sampling overhead: 81.0
Subobservables: {'A': PauliList(['II', 'ZI', 'ZZ', 'XI', 'ZZ', 'IX']), 'B': PauliList(['ZZ', 'IZ', 'II', 'XI', 'ZI', 'IX'])}
subcircuits["B"].draw("mpl", scale=0.8)
O próximo passo é usar os subcircuitos e subobserváveis para gerar os subexperimentos a serem executados em uma QPU usando o método generate_cutting_experiments.
Para estimar o valor esperado do circuito completo, muitos subexperimentos são gerados a partir da distribuição de quase-probabilidade conjunta dos gates decompostos e, em seguida, executados em uma ou mais QPUs. O número de amostras a serem retiradas dessa distribuição é controlado pelo argumento num_samples.
O bloco de código a seguir gera os subexperimentos e os executa usando a primitiva Sampler em um simulador local. (Para executá-los em uma QPU, altere o backend para o recurso QPU de sua escolha.)
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=4096 * 3)
for label, subsystem_subexpts in isa_subexperiments.items()
}
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
Por último, o valor esperado do circuito completo é 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.
# Get expectation values for each observable term
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
# Reconstruct final expectation value
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
estimator = EstimatorV2()
exact_expval = (
estimator.run([(qc, observable, [0.4] * len(qc.parameters))])
.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: 0.53571896
Exact expectation value: 0.56254612
Error in estimation: -0.02682716
Relative error in estimation: -0.04768882
Próximos passos
- Leia o guia Comece a usar o corte de circuitos com cortes de fio.
- Leia o artigo no arXiv sobre costura de circuitos com comunicação clássica.