Taglio di circuiti per la riduzione della profondità
Stima di utilizzo: Otto minuti su un processore Eagle (NOTA: Questa è solo una stima. Il tuo tempo di esecuzione potrebbe variare.)
Contesto
Questo tutorial dimostra come costruire un pattern Qiskit per tagliare le porte in un circuito quantistico al fine di ridurre la profondità del circuito. Per una discussione più approfondita sul taglio dei circuiti, visitate la documentazione dell'addon Qiskit per il taglio di circuiti.
Requisiti
Prima di iniziare questo tutorial, assicurati di avere installato quanto segue:
- Qiskit SDK v2.0 o successivo, con supporto per la visualizzazione
- Qiskit Runtime v0.22 o successivo (
pip install qiskit-ibm-runtime) - Addon Qiskit per il taglio di circuiti v0.9.0 o successivo (
pip install qiskit-addon-cutting)
Configurazione
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-ibm-runtime
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, SamplerV2
Passo 1: Mappare gli input classici a un problema quantistico
Implementeremo il nostro pattern Qiskit utilizzando i quattro passaggi delineati nella documentazione. In questo caso, simuleremo i valori di aspettazione su un circuito di una certa profondità tagliando le porte che risultano in porte swap ed eseguendo sottoesperimenti su circuiti meno profondi. Il taglio delle porte è rilevante per i Passaggi 2 (ottimizzare il circuito per l'esecuzione quantistica decomponendo le porte distanti) e 4 (post-elaborazione per ricostruire i valori di aspettazione sul circuito originale). Nel primo passaggio, genereremo un circuito dalla libreria di circuiti Qiskit e definiremo alcune osservabili.
- Input: Parametri classici per definire un circuito
- Output: Circuito astratto e osservabili
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")
Passo 2: Ottimizzare il problema per l'esecuzione su hardware quantistico
- Input: Circuito astratto e osservabili
- Output: Circuito target e osservabili prodotti tagliando le porte distanti per ridurre la profondità del circuito traspilato
Scegliamo un layout iniziale che richiede due swap per eseguire le porte tra i qubit 3 e 0 e altri due swap per riportare i qubit alle loro posizioni iniziali. Scegliamo optimization_level=3, che è il livello più alto di ottimizzazione disponibile con un gestore di passaggi preimpostato.
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)
Transpiled circuit depth: 103
Trovare e tagliare le porte distanti: Sostituiremo le porte distanti (porte che collegano qubit non locali, 0 e 3) con oggetti TwoQubitQPDGate specificando i loro indici. cut_gates sostituirà le porte negli indici specificati con oggetti TwoQubitQPDGate e restituirà anche un elenco di istanze QPDBasis -- una per ciascuna decomposizione di porta. L'oggetto QPDBasis contiene informazioni su come decomporre le porte tagliate in operazioni a singolo qubit.
# 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)
Generare i sottoesperimenti da eseguire sul backend: generate_cutting_experiments accetta un circuito contenente istanze di TwoQubitQPDGate e osservabili come PauliList.
Per simulare il valore di aspettazione del circuito completo, molti sottoesperimenti vengono generati dalla distribuzione di quasiprobabilità congiunta delle porte decomposte e poi eseguiti su uno o più backend. Il numero di campioni prelevati dalla distribuzione è controllato da num_samples, e viene fornito un coefficiente combinato per ciascun campione unico. Per maggiori informazioni su come vengono calcolati i coefficienti, fai riferimento al materiale esplicativo.
# Generate the subexperiments and sampling coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=qpd_circuit, observables=observables, num_samples=np.inf
)
Per confronto, vediamo che i sottoesperimenti QPD saranno meno profondi dopo il taglio delle porte distanti: Ecco un esempio di un sottoesperimento scelto arbitrariamente generato dal circuito QPD. La sua profondità è stata ridotta di più della metà. Molti di questi sottoesperimenti probabilistici devono essere generati e valutati per ricostruire un valore di aspettazione del circuito più profondo.
# 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
)
Original circuit depth after transpile: 103
QPD subexperiment depth after transpile: 46
D'altra parte, il taglio comporta la necessità di campionamento aggiuntivo. Qui tagliamo tre porte CNOT, con un sovraccarico di campionamento risultante di . Per maggiori informazioni sul sovraccarico di campionamento causato dal taglio di circuiti, fai riferimento alla documentazione del Circuit Knitting Toolbox.
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
Sampling overhead: 729.0
Passo 3: Eseguire utilizzando le primitive Qiskit
Eseguire i circuiti target ("sottoesperimenti") con la Primitiva Sampler.
- Input: Circuiti target
- Output: Distribuzioni di quasiprobabilità
# 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())
czypg1r6rr3g008mgp6g
Passo 4: Post-elaborare e restituire il risultato nel formato classico desiderato
Utilizzare i risultati dei sottoesperimenti, le sotto-osservabili e i coefficienti di campionamento per ricostruire il valore di aspettazione del circuito originale.
Input: Distribuzioni di quasiprobabilità Output: Valori di aspettazione ricostruiti
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)
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)
Ideal expectation value
1.2283177520039992
Sondaggio sul tutorial
Ti preghiamo di compilare questo breve sondaggio per fornire feedback su questo tutorial. Il tuo feedback ci aiuterà a migliorare i nostri contenuti e l'esperienza utente.