Corte de circuito para redução de profundidade
Estimativa de uso: Oito minutos em um processador Eagle (OBSERVAÇÃO: esta é apenas uma estimativa. Seu tempo de execução pode variar)
Segundo plano
Este tutorial demonstra como construir um Qiskit pattern para cortar portas em um circuito quântico para reduzir a profundidade do circuito. Para uma discussão mais aprofundada sobre o corte de circuitos, visite a documentação do addon Qiskit sobre corte de circuitos.
Requisitos
Antes de iniciar este tutorial, verifique se você tem os seguintes itens instalados:
- Qiskit SDK v2.0 ou posterior, com suporte para visualização
- Qiskit Runtime v0.22 ou mais tarde (
pip install qiskit-ibm-runtime) - Complemento Qiskit para corte de circuitos v0.9.0 ou posterior (
pip install qiskit-addon-cutting)
Instalação
import numpy as np
from qiskit.circuit.library import EfficientSU2
from qiskit.quantum_info import PauliList, Statevector, SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_addon_cutting import (
cut_gates,
generate_cutting_experiments,
reconstruct_expectation_values,
)
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2Passo 1: Mapear entradas clássicas para um problema quântico
Implementaremos nosso padrão Qiskit usando as quatro etapas descritas na documentação. Nesse caso, simularemos valores de expectativa em um circuito de certa profundidade cortando portas que resultem em portas de troca e executando subexperimentos em circuitos mais rasos. O corte de portas é relevante para as etapas 2 (otimização do circuito para execução quântica por meio da decomposição de portas distantes) e 4 (pós-processamento para reconstruir valores de expectativa no circuito original). Na primeira etapa, geraremos um circuito a partir da biblioteca de circuitos do Qiskit e definiremos alguns observáveis.
- Entrada: Parâmetros clássicos para definir um circuito
- Saída: Circuito abstrato e observáveis
circuit = EfficientSU2(num_qubits=4, entanglement="circular").decompose()
circuit.assign_parameters([0.4] * len(circuit.parameters), inplace=True)
observables = PauliList(["ZZII", "IZZI", "IIZZ", "XIXI", "ZIZZ", "IXIX"])
circuit.draw("mpl", scale=0.8, style="iqp")Output:
Etapa 2: Otimizar o problema para execução em hardware quântico
- Entrada: Circuito abstrato e observáveis
- Resultado: Circuito-alvo e observáveis produzidos pelo corte de portas distantes para reduzir a profundidade do circuito transpilado
Escolhemos um layout inicial que requer duas trocas para executar as portas entre os qubits 3 e 0 e outras duas trocas para retornar os qubits às suas posições iniciais. Escolhemos optimization_level=3, que é o nível mais alto de otimização disponível com um gerenciador de passes predefinido.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, min_num_qubits=circuit.num_qubits, simulator=False
)
pm = generate_preset_pass_manager(
optimization_level=3, initial_layout=[0, 1, 2, 3], backend=backend
)
transpiled_qc = pm.run(circuit)
print(f"Transpiled circuit depth: {transpiled_qc.depth()}")
transpiled_qc.draw("mpl", scale=0.4, idle_wires=False, style="iqp", fold=-1)Output:
Transpiled circuit depth: 103
Encontre e corte os portões distantes: Substituiremos os portões distantes (portões que conectam qubits não locais, 0 e 3) por TwoQubitQPDGate objetos, especificando seus índices. cut_gates substituirá os portões nos índices especificados por TwoQubitQPDGate objetos e também retornará uma lista de QPDBasis instâncias — uma para cada decomposição de portão. O QPDBasis objeto contém informações sobre como decompor os portões de corte em operações de qubit único.
# 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)Output:
Gere os subexperimentos a serem executados no backend : generate_cutting_experiments aceita um circuito contendo instâncias de TwoQubitQPDGate e observáveis como um PauliList.
Para simular o valor esperado do circuito de tamanho normal, muitos subexperimentos são gerados a partir da distribuição de quase-robabilidade conjunta das portas decompostas e, em seguida, executados em um ou mais backends. O número de amostras retiradas da distribuição é controlado por num_samples, e um coeficiente combinado é fornecido para cada amostra exclusiva. Para obter mais informações sobre como os coeficientes são calculados, consulte o material explicativo.
# Generate the subexperiments and sampling coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=qpd_circuit, observables=observables, num_samples=np.inf
)Para fins de comparação, vemos que as subexperiências QPD serão mais rasas depois de cortar portas distantes : Aqui está um exemplo de uma subexperiência escolhida arbitrariamente e gerada 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 de expectativa do circuito mais profundo.
# Transpile the decomposed circuit to the same layout
transpiled_qpd_circuit = pm.run(subexperiments[100])
print(f"Original circuit depth after transpile: {transpiled_qc.depth()}")
print(
f"QPD subexperiment depth after transpile: {transpiled_qpd_circuit.depth()}"
)
transpiled_qpd_circuit.draw(
"mpl", scale=0.6, style="iqp", idle_wires=False, fold=-1
)Output:
Original circuit depth after transpile: 103
QPD subexperiment depth after transpile: 46
Por outro lado, o corte resulta na necessidade de amostragem extra. Aqui, cortamos três portas CNOT, o que resulta em uma sobrecarga de amostragem de . Para saber mais sobre a sobrecarga de amostragem incorrida pelo corte do circuito, consulte a documentação do Circuit Knitting Toolbox.
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")Output:
Sampling overhead: 729.0
Passo 3: Execute usando Qiskit primitives
Execute os circuitos-alvo (“subexperimentos”) com a primitiva Sampler.
- Entrada: Circuitos-alvo
- Saída: Distribuições de quase-probabilidade
# Transpile the subexperiments to the backend's instruction set architecture (ISA)
isa_subexperiments = pm.run(subexperiments)
# 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()print(job.job_id())Output:
czypg1r6rr3g008mgp6g
Etapa 4: Pós-processamento e retorno do resultado no formato clássico desejado
Use os resultados do subexperimento, os subobserváveis e os coeficientes de amostragem para reconstruir o valor esperado do circuito original.
Entrada: Distribuições de quase-probabilidade Saída: Valores de expectativa reconstruídos
reconstructed_expvals = reconstruct_expectation_values(
results,
coefficients,
observables,
)
# Reconstruct final expectation value
final_expval = np.dot(reconstructed_expvals, [1] * len(observables))
print("Final reconstructed expectation value")
print(final_expval)Output:
Final reconstructed expectation value
1.0751342773437473
ideal_expvals = [
Statevector(circuit).expectation_value(SparsePauliOp(observable))
for observable in observables
]
print("Ideal expectation value")
print(np.dot(ideal_expvals, [1] * len(observables)).real)Output:
Ideal expectation value
1.2283177520039992
Pesquisa tutorial
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 a experiência do usuário.