Gate Cutting para Reduzir a Profundidade do Circuito
Neste tutorial, vamos reduzir a profundidade de um circuito cortando portas distantes, evitando as portas swap que de outra forma seriam introduzidas pelo roteamento.
Estas são as etapas que vamos seguir neste padrão Qiskit:
- Etapa 1: Mapear o problema para circuitos quânticos e operadores:
- Mapear o hamiltoniano em um circuito quântico.
- Etapa 2: Otimizar para o hardware alvo [Usa o cutting addon]:
- Cortar o circuito e o observável.
- Transpilar os subexperimentos para o hardware.
- Etapa 3: Executar no hardware alvo:
- Executar os subexperimentos obtidos na Etapa 2 usando uma primitiva
Sampler.
- Executar os subexperimentos obtidos na Etapa 2 usando uma primitiva
- Etapa 4: Pós-processar os resultados [Usa o cutting addon]:
- Combinar os resultados da Etapa 3 para reconstruir o valor esperado do observável em questão.
Etapa 1: Mapear
Crie um circuito para executar no backend
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-aer qiskit-ibm-runtime
from qiskit.circuit.library import efficient_su2
circuit = efficient_su2(num_qubits=4, entanglement="circular")
circuit.assign_parameters([0.4] * len(circuit.parameters), inplace=True)
circuit.draw("mpl", scale=0.8)

Especifique um observável
from qiskit.quantum_info import SparsePauliOp
observable = SparsePauliOp(["ZZII", "IZZI", "-IIZZ", "XIXI", "ZIZZ", "IXIX"])
Etapa 2: Otimizar
Especifique um backend
Você pode fornecer um backend simulado (fake) ou um backend de hardware do Qiskit Runtime.
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
backend = FakeManilaV2()
Transpile o circuito, visualize os swaps e observe a profundidade
Escolhemos um layout que requer dois swaps para executar as portas entre os qubits 3 e 0 e mais dois swaps para retornar os qubits às suas posições iniciais.
from qiskit.transpiler import generate_preset_pass_manager
pass_manager = generate_preset_pass_manager(
optimization_level=1, backend=backend, initial_layout=[0, 1, 2, 3]
)
transpiled_qc = pass_manager.run(circuit)
print(f"Transpiled circuit depth: {transpiled_qc.depth(lambda x: len(x.qubits) >= 2)}")
Transpiled circuit depth: 30
transpiled_qc.draw("mpl", scale=0.4, idle_wires=False, fold=-1)

Substitua portas distantes por TwoQubitQPDGates especificando seus índices
cut_gates substituirá as portas nos índices especificados por TwoQubitQPDGates e também retornará uma lista de instâncias de QPDBasis -- uma para cada decomposição de porta.
from qiskit_addon_cutting import cut_gates
# 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)

Gere os subexperimentos para executar no backend
generate_cutting_experiments aceita um circuito contendo instâncias de TwoQubitQPDGate e observáveis como uma PauliList.
Para simular o valor esperado do circuito de tamanho completo, muitos subexperimentos são gerados a partir da distribuição de quasiprobabilidade conjunta das portas decompostas e então executados em um ou mais backends. O número de amostras retiradas da distribuição é controlado por num_samples, e um coeficiente combinado é dado para cada amostra única. Para mais informações sobre como os coeficientes são calculados, consulte o material explicativo.
Nota: O kwarg observables de generate_cutting_experiments é 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.
import numpy as np
from qiskit_addon_cutting import generate_cutting_experiments
# Generate the subexperiments and sampling coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=qpd_circuit, observables=observable.paulis, num_samples=np.inf
)
Calcule o sampling overhead para os cortes escolhidos
Aqui cortamos três portas CNOT, resultando em um sampling overhead de .
Para mais detalhes sobre o sampling overhead incorrido pelo circuit cutting, consulte o material explicativo.
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
Sampling overhead: 729.0
Demonstre que os subexperimentos QPD serão mais rasos após cortar portas distantes
Aqui está um exemplo de um subexperimento escolhido arbitrariamente, gerado a partir do circuito QPD. Sua profundidade foi reduzida em mais da metade. Muitos desses subexperimentos probabilísticos devem ser gerados e avaliados para reconstruir um valor esperado do circuito mais profundo.
# Transpile the decomposed circuit to the same layout
transpiled_qpd_circuit = pass_manager.run(subexperiments[100])
print(
f"Original circuit depth after transpile: {transpiled_qc.depth(lambda x: len(x.qubits) >= 2)}"
)
print(
f"QPD subexperiment depth after transpile: {transpiled_qpd_circuit.depth(lambda x: len(x.qubits) >= 2)}"
)
transpiled_qpd_circuit.draw("mpl", scale=0.8, idle_wires=False, fold=-1)
Original circuit depth after transpile: 30
QPD subexperiment depth after transpile: 7

Prepare os subexperimentos para o backend
# Transpile the subeperiments to the backend's instruction set architecture (ISA)
isa_subexperiments = pass_manager.run(subexperiments)
Etapa 3: Executar
Execute os subexperimentos usando a primitiva Sampler do Qiskit Runtime
from qiskit_ibm_runtime import SamplerV2
# Set up the Qiskit Runtime Sampler primitive. For a fake backend, this will use a local simulator.
sampler = SamplerV2(backend)
# Submit the subexperiments
job = sampler.run(isa_subexperiments)
# Retrieve the results
results = job.result()
Etapa 4: Pós-processar
Reconstrua o valor esperado
Reconstrua os valores esperados para cada termo do observável e combine-os para reconstruir o valor esperado do observável original.
from qiskit_addon_cutting import reconstruct_expectation_values
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
observable.paulis,
)
# Reconstruct final expectation value
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
Compare o valor esperado reconstruído com o valor esperado exato do circuito e observável originais
from qiskit_aer.primitives import EstimatorV2
estimator = EstimatorV2()
exact_expval = estimator.run([(circuit, 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: 0.44018555
Exact expectation value: 0.50497603
Error in estimation: -0.06479049
Relative error in estimation: -0.12830408