Vai al contenuto principale

Confrontare le impostazioni del transpiler

Stima di utilizzo: meno di un minuto su un processore Eagle r3 (NOTA: questa è solo una stima. I tempi effettivi potrebbero variare.)

Background​

Per garantire risultati più rapidi ed efficienti, a partire dal 1° marzo 2024 i circuiti e gli osservabili devono essere trasformati in modo da utilizzare esclusivamente le istruzioni supportate dalla QPU (unità di elaborazione quantistica) prima di essere inviati alle primitive di Qiskit Runtime. Questi vengono chiamati circuiti e osservabili ISA (instruction set architecture). Un modo comune per farlo è utilizzare la funzione generate_preset_pass_manager del transpiler. Tuttavia, potresti scegliere di seguire un processo più manuale.

Ad esempio, potresti voler puntare a un sottoinsieme specifico di qubit su un determinato dispositivo. Questa procedura guidata verifica le prestazioni di diverse impostazioni del transpiler completando l'intero processo di creazione, transpilazione e invio di circuiti.

Requisiti​

Prima di iniziare, assicurati di avere installato quanto segue:

  • Qiskit SDK v1.2 o versioni successive, con supporto per la visualizzazione
  • Qiskit Runtime v0.28 o versioni successive (pip install qiskit-ibm-runtime)

Setup​

# Added by doQumentation — required packages for this notebook
!pip install -q qiskit qiskit-ibm-runtime
# Create circuit to test transpiler on
from qiskit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.circuit.library import GroverOperator, Diagonal

# Use Statevector object to calculate the ideal output
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_histogram
from qiskit.transpiler import PassManager

from qiskit.circuit.library import XGate
from qiskit.quantum_info import hellinger_fidelity

# Qiskit Runtime
from qiskit_ibm_runtime import (
QiskitRuntimeService,
Batch,
SamplerV2 as Sampler,
)
from qiskit_ibm_runtime.transpiler.passes.scheduling import (
ASAPScheduleAnalysis,
PadDynamicalDecoupling,
)

Passo 1: Mappare gli input classici in un problema quantistico​

Crea un piccolo circuito che il transpiler proverà a ottimizzare. In questo esempio viene creato un circuito che esegue l'algoritmo di Grover con un oracolo che contrassegna lo stato 111. Poi simula la distribuzione ideale (quella che ti aspetteresti di misurare eseguendo il circuito su un computer quantistico perfetto un numero infinito di volte) per un confronto successivo.

# To run on hardware, select the backend with the fewest number of jobs in the queue
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
backend.name
'ibm_brisbanse'
oracle = Diagonal([1] * 7 + [-1])
qc = QuantumCircuit(3)
qc.h([0, 1, 2])
qc = qc.compose(GroverOperator(oracle))

qc.draw(output="mpl", style="iqp")

Output of the previous code cell

ideal_distribution = Statevector.from_instruction(qc).probabilities_dict()

plot_histogram(ideal_distribution)

Output of the previous code cell

Passo 2: Ottimizzare il problema per l'esecuzione su hardware quantistico​

Ora transpila i circuiti per la QPU. Confronterai le prestazioni del transpiler con optimization_level impostato a 0 (minimo) rispetto a 3 (massimo). Il livello di ottimizzazione minimo fa il minimo indispensabile per far girare il circuito sul dispositivo: mappa i qubit del circuito sui qubit del dispositivo e aggiunge gate di swap per consentire tutte le operazioni a due qubit. Il livello di ottimizzazione massimo è molto più intelligente e utilizza numerosi accorgimenti per ridurre il numero totale di gate. Poiché i gate a più qubit hanno tassi di errore elevati e i qubit decoerono nel tempo, i circuiti più corti dovrebbero produrre risultati migliori.

La cella seguente transpila qc per entrambi i valori di optimization_level, stampa il numero di gate a due qubit e aggiunge i circuiti transpilati a una lista. Alcuni algoritmi del transpiler sono randomizzati, quindi viene impostato un seed per la riproducibilità.

# Need to add measurements to the circuit
qc.measure_all()

# Find the correct two-qubit gate
twoQ_gates = set(["ecr", "cz", "cx"])
for gate in backend.basis_gates:
if gate in twoQ_gates:
twoQ_gate = gate

circuits = []
for optimization_level in [0, 3]:
pm = generate_preset_pass_manager(
optimization_level, backend=backend, seed_transpiler=0
)
t_qc = pm.run(qc)
print(
f"Two-qubit gates (optimization_level={optimization_level}): ",
t_qc.count_ops()[twoQ_gate],
)
circuits.append(t_qc)
Two-qubit gates (optimization_level=0):  21
Two-qubit gates (optimization_level=3): 14

Poiché i CNOT hanno generalmente un tasso di errore elevato, il circuito transpilato con optimization_level=3 dovrebbe ottenere prestazioni molto migliori.

Un altro modo per migliorare le prestazioni è il dynamic decoupling, che consiste nell'applicare una sequenza di gate ai qubit in attesa. Questo annulla alcune interazioni indesiderate con l'ambiente. La cella seguente aggiunge il dynamic decoupling al circuito transpilato con optimization_level=3 e lo aggiunge alla lista.

# Get gate durations so the transpiler knows how long each operation takes
durations = backend.target.durations()

# This is the sequence we'll apply to idling qubits
dd_sequence = [XGate(), XGate()]

# Run scheduling and dynamic decoupling passes on circuit
pm = PassManager(
[
ASAPScheduleAnalysis(durations),
PadDynamicalDecoupling(durations, dd_sequence),
]
)
circ_dd = pm.run(circuits[1])

# Add this new circuit to our list
circuits.append(circ_dd)
circ_dd.draw(output="mpl", style="iqp", idle_wires=False)

Output of the previous code cell

Passo 3: Eseguire con le primitive di Qiskit​

A questo punto hai una lista di circuiti transpilati per la QPU specificata. Ora crea un'istanza della primitiva sampler e avvia un job in batch usando il context manager (with ...:), che apre e chiude il batch automaticamente.

All'interno del context manager, campiona i circuiti e salva i risultati in result.

with Batch(backend=backend):
sampler = Sampler()
job = sampler.run(
[(circuit) for circuit in circuits], # sample all three circuits
shots=8000,
)
result = job.result()

Passo 4: Post-elaborare e restituire il risultato nel formato classico desiderato​

Per concludere, visualizza i risultati delle esecuzioni sul dispositivo rispetto alla distribuzione ideale. Puoi vedere che i risultati con optimization_level=3 sono più vicini alla distribuzione ideale grazie al minor numero di gate, e optimization_level=3 + dd è ancora più vicino grazie al dynamic decoupling.

binary_prob = [
{
k: v / res.data.meas.num_shots
for k, v in res.data.meas.get_counts().items()
}
for res in result
]
plot_histogram(
binary_prob + [ideal_distribution],
bar_labels=False,
legend=[
"optimization_level=0",
"optimization_level=3",
"optimization_level=3 + dd",
"ideal distribution",
],
)

Output of the previous code cell

Puoi confermare questo calcolando la fedeltà di Hellinger tra ogni insieme di risultati e la distribuzione ideale (più alto è meglio, e 1 corrisponde alla fedeltà perfetta).

for prob in binary_prob:
print(f"{hellinger_fidelity(prob, ideal_distribution):.3f}")
0.848
0.945
0.990