Vai al contenuto principale

Combinare opzioni di mitigazione degli errori con la primitiva Estimator

Stima di utilizzo: Sette minuti su un processore Heron r2 (NOTA: Questa è solo una stima. Il tuo tempo di esecuzione potrebbe variare.)

Contesto

Questa guida esplora le opzioni di soppressione e mitigazione degli errori disponibili con la primitiva Estimator di Qiskit Runtime. Costruirai un circuito e un osservabile e sottometterai job utilizzando la primitiva Estimator con diverse combinazioni di impostazioni di mitigazione degli errori. Quindi, visualizzerai i risultati per osservare gli effetti delle varie impostazioni. La maggior parte degli esempi utilizza un circuito a 10 qubit per rendere più facili le visualizzazioni, e alla fine potrai scalare il workflow fino a 50 qubit.

Queste sono le opzioni di soppressione e mitigazione degli errori che utilizzerai:

  • Disaccoppiamento dinamico
  • Mitigazione degli errori di misura
  • gate twirling
  • Estrapolazione a rumore zero (ZNE)

Requisiti

Prima di iniziare questa guida, assicurati di avere installato quanto segue:

  • Qiskit SDK v2.1 o successivo, con supporto per la visualizzazione
  • Qiskit Runtime v0.40 o successivo (pip install qiskit-ibm-runtime)

Configurazione

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime
import matplotlib.pyplot as plt
import numpy as np

from qiskit.circuit.library import efficient_su2, unitary_overlap
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Batch, EstimatorV2 as Estimator

Passo 1: Mappare input classici a un problema quantistico

Questa guida assume che il problema classico sia già stato mappato in forma quantistica. Inizia costruendo un circuito e un osservabile da misurare. Sebbene le tecniche qui utilizzate si applichino a molti tipi diversi di circuiti, per semplicità questa guida utilizza il circuito efficient_su2 incluso nella libreria di circuiti di Qiskit.

efficient_su2 è un circuito quantistico parametrizzato progettato per essere eseguito in modo efficiente su hardware quantistico con connettività limitata dei qubit, pur essendo sufficientemente espressivo per risolvere problemi in domini applicativi come l'ottimizzazione e la chimica. È costruito alternando strati di gate parametrizzati a singolo qubit con uno strato contenente un pattern fisso di gate a due qubit, per un numero scelto di ripetizioni. Il pattern di gate a due qubit può essere specificato dall'utente. Qui puoi utilizzare il pattern integrato pairwise perché minimizza la profondità del circuito impacchettando i gate a due qubit nel modo più denso possibile. Questo pattern può essere eseguito utilizzando solo connettività lineare dei qubit.

n_qubits = 10
reps = 1

circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)

circuit.decompose().draw("mpl", scale=0.7)

Output of the previous code cell

Output of the previous code cell

Per il nostro osservabile, prendiamo l'operatore di Pauli ZZ che agisce sull'ultimo qubit, ZIIZ I \cdots I.

# Z on the last qubit (index -1) with coefficient 1.0
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)

A questo punto, potresti procedere a eseguire il tuo circuito e misurare l'osservabile. Tuttavia, vuoi anche confrontare l'output del dispositivo quantistico con la risposta corretta - cioè, il valore teorico dell'osservabile, se il circuito fosse stato eseguito senza errori. Per circuiti quantistici piccoli puoi calcolare questo valore simulando il circuito su un computer classico, ma questo non è possibile per circuiti più grandi, su scala di utilità. Puoi aggirare questo problema con la tecnica del "circuito specchiato" (nota anche come "compute-uncompute"), che è utile per il benchmarking delle prestazioni dei dispositivi quantistici.

Circuito specchiato

Nella tecnica del circuito specchiato, concatena il circuito con il suo circuito inverso, che è formato invertendo ciascun gate del circuito in ordine inverso. Il circuito risultante implementa l'operatore identità, che può essere simulato in modo banale. Poiché la struttura del circuito originale è preservata nel circuito specchiato, eseguire il circuito specchiato dà comunque un'idea di come il dispositivo quantistico si comporterebbe sul circuito originale.

La seguente cella di codice assegna parametri casuali al tuo circuito, e quindi costruisce il circuito specchiato utilizzando la classe unitary_overlap. Prima di specchiare il circuito, aggiungi un'istruzione barrier ad esso per impedire al transpiler di fondere le due parti del circuito su entrambi i lati della barriera. Senza la barriera, il transpiler fonderebbe il circuito originale con il suo inverso, risultando in un circuito traspilato senza alcun gate.

# Generate random parameters
rng = np.random.default_rng(1234)
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)

# Assign the parameters to the circuit
assigned_circuit = circuit.assign_parameters(params)

# Add a barrier to prevent circuit optimization of mirrored operators
assigned_circuit.barrier()

# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)

mirror_circuit.decompose().draw("mpl", scale=0.7)

Output of the previous code cell

Output of the previous code cell

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

Devi ottimizzare il tuo circuito prima di eseguirlo sull'hardware. Questo processo comporta alcuni passi:

  • Scegliere un layout di qubit che mappi i qubit virtuali del tuo circuito ai qubit fisici sull'hardware.
  • Inserire gate di swap secondo necessità per instradare interazioni tra qubit che non sono connessi.
  • Tradurre i gate nel tuo circuito in istruzioni Instruction Set Architecture (ISA) che possono essere eseguite direttamente sull'hardware.
  • Eseguire ottimizzazioni del circuito per minimizzare la profondità del circuito e il conteggio dei gate.

Il transpiler integrato in Qiskit può eseguire tutti questi passi per te. Poiché questo esempio utilizza un circuito hardware-efficient, il transpiler dovrebbe essere in grado di scegliere un layout di qubit che non richieda l'inserimento di alcun gate di swap per l'instradamento delle interazioni.

Devi scegliere il dispositivo hardware da utilizzare prima di ottimizzare il tuo circuito. La seguente cella di codice richiede il dispositivo meno occupato con almeno 127 qubit.

service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)

Puoi traspilare il tuo circuito per il tuo backend scelto creando un pass manager e quindi eseguendo il pass manager sul circuito. Un modo semplice per creare un pass manager è utilizzare la funzione generate_preset_pass_manager. Consulta Transpile with pass managers per una spiegazione più dettagliata della traspilazione con i pass manager.

pass_manager = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=1234
)
isa_circuit = pass_manager.run(mirror_circuit)

isa_circuit.draw("mpl", idle_wires=False, scale=0.7, fold=-1)

Output of the previous code cell

Output of the previous code cell

Il circuito traspilato ora contiene solo istruzioni ISA. I gate a singolo qubit sono stati decomposti in termini di gate X\sqrt{X} e rotazioni RzR_z, e i gate CX sono stati decomposti in gate ECR e rotazioni a singolo qubit.

Il processo di traspilazione ha mappato i qubit virtuali del circuito ai qubit fisici sull'hardware. Le informazioni sul layout dei qubit sono memorizzate nell'attributo layout del circuito traspilato. Anche l'osservabile è stato definito in termini di qubit virtuali, quindi devi applicare questo layout all'osservabile, cosa che puoi fare con il metodo apply_layout di SparsePauliOp.

isa_observable = observable.apply_layout(isa_circuit.layout)

print("Original observable:")
print(observable)
print()
print("Observable with layout applied:")
print(isa_observable)
Original observable:
SparsePauliOp(['ZIIIIIIIII'],
coeffs=[1.+0.j])

Observable with layout applied:
SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])

Passo 3: Eseguire utilizzando le primitive Qiskit

Ora sei pronto a eseguire il tuo circuito utilizzando la primitiva Estimator.

Qui sottometterai cinque job separati, iniziando senza soppressione o mitigazione degli errori, e abilitando successivamente varie opzioni di soppressione e mitigazione degli errori disponibili in Qiskit Runtime. Per informazioni sulle opzioni, fai riferimento alle seguenti pagine:

Poiché questi job possono essere eseguiti indipendentemente l'uno dall'altro, puoi utilizzare la modalità batch per consentire a Qiskit Runtime di ottimizzare la temporizzazione della loro esecuzione.

pub = (isa_circuit, isa_observable)

jobs = []

with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0

# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)

# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)

# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)

# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)

# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)

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

Infine, puoi analizzare i dati. Qui recupererai i risultati dei job, estrarrai i valori di aspettazione misurati da essi e visualizzerai i valori, includendo barre di errore di una deviazione standard.

# Retrieve the job results
results = [job.result() for job in jobs]

# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]

# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)

# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")

plt.show()

Output of the previous code cell

A questa piccola scala, è difficile vedere l'effetto della maggior parte delle tecniche di mitigazione degli errori, ma l'estrapolazione a rumore zero fornisce un miglioramento evidente. Tuttavia, nota che questo miglioramento non arriva gratuitamente, perché il risultato ZNE ha anche una barra di errore più grande.

Scalare l'esperimento

Quando si sviluppa un esperimento, è utile iniziare con un circuito piccolo per rendere più facili le visualizzazioni e le simulazioni. Ora che hai sviluppato e testato il workflow su un circuito a 10 qubit, puoi scalarlo fino a 50 qubit. La seguente cella di codice ripete tutti i passi di questa guida, ma ora li applica a un circuito a 50 qubit.

n_qubits = 50
reps = 1

# Construct circuit and observable
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)

# Assign parameters to circuit
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
assigned_circuit = circuit.assign_parameters(params)
assigned_circuit.barrier()

# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)

# Transpile circuit and observable
isa_circuit = pass_manager.run(mirror_circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)

# Run jobs
pub = (isa_circuit, isa_observable)

jobs = []

with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0

# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)

# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)

# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)

# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)

# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)

# Retrieve the job results
results = [job.result() for job in jobs]

# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]

# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)

# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")

plt.show()

Output of the previous code cell

Quando confronti i risultati a 50 qubit con i risultati a 10 qubit precedenti, potresti notare quanto segue (i tuoi risultati potrebbero differire tra le esecuzioni):

  • I risultati senza mitigazione degli errori sono peggiori. Eseguire il circuito più grande comporta l'esecuzione di più gate, quindi ci sono più opportunità per gli errori di accumularsi.
  • L'aggiunta del disaccoppiamento dinamico potrebbe aver peggiorato le prestazioni. Questo non è sorprendente, perché il circuito è molto denso. Il disaccoppiamento dinamico è principalmente utile quando ci sono grandi lacune nel circuito durante le quali i qubit rimangono inattivi senza che vengano applicati gate. Quando queste lacune non sono presenti, il disaccoppiamento dinamico non è efficace e può effettivamente peggiorare le prestazioni a causa di errori nei pulse di disaccoppiamento dinamico stessi. Il circuito a 10 qubit potrebbe essere stato troppo piccolo perché potessimo osservare questo effetto.
  • Con l'estrapolazione a rumore zero, il risultato è buono quanto, o quasi quanto, il risultato a 10 qubit, sebbene la barra di errore sia molto più grande. Questo dimostra la potenza della tecnica ZNE!

Conclusione

In questa guida, hai investigato diverse opzioni di mitigazione degli errori disponibili per la primitiva Estimator di Qiskit Runtime. Hai sviluppato un workflow utilizzando un circuito a 10 qubit, e quindi l'hai scalato fino a 50 qubit. Potresti aver osservato che abilitare più opzioni di soppressione e mitigazione degli errori non migliora sempre le prestazioni (in particolare, l'abilitazione del disaccoppiamento dinamico in questo caso). La maggior parte delle opzioni accetta configurazione aggiuntiva, che puoi testare nel tuo lavoro!