Skip to main content
IBM Quantum Platform

Combinare le opzioni di mitigazione degli errori con la primitiva Stimatore

Stima della durata: 7 minuti su un processore Heron r2 (NOTA: si tratta solo di una stima. (La durata potrebbe variare.)


Risultati di apprendimento

Consigliamo agli utenti di acquisire familiarità con i seguenti argomenti prima di seguire questo tutorial:

  • I fondamenti del disaccoppiamento dinamico, della mitigazione degli errori di misurazione, del gate twirling e dell'estrapolazione a rumore zero, come descritto nella presente guida.

Prerequisiti

Dopo aver seguito questo tutorial, gli utenti dovrebbero aver compreso:

  • In che modo le suddette tecniche di mitigazione degli errori vengono implementate in modo selettivo a livello hardware.
  • Come si confrontano in termini di capacità di attenuare il rumore dell'hardware.

Sfondo

Questo tutorial illustra le opzioni di soppressione e mitigazione degli errori disponibili con la primitiva " IBM Quantum " di Estimator. Questo tutorial illustra come implementare singolarmente ciascuno dei seguenti metodi:

  • Disaccoppiamento dinamico
  • Attenuazione dell'errore di misura
  • Porta girevole
  • Estrapolazione a rumore zero (ZNE)

Si noti che, invece di implementare queste tecniche singolarmente, è possibile implementarle utilizzando un livello di resilienza, che resilience_level può assumere i valori 0, 1, 2:

  • 0: Non è stata implementata alcuna misura di mitigazione.
  • 1: È stata implementata la riduzione degli errori di misurazione.
  • 2: Sono state implementate le funzioni di rotazione del cancello, la riduzione degli errori di misurazione e lo ZNE.

In questo tutorial, realizzerai un circuito e un osservabile e invierai dei lavori utilizzando la primitiva Estimator con diverse combinazioni di impostazioni di mitigazione dell'errore. Successivamente, rappresenterai graficamente i risultati per osservare gli effetti delle diverse impostazioni. La maggior parte del tutorial utilizza un circuito da 10 qubit per facilitare la comprensione visiva; alla fine, estenderai il flusso di lavoro a 50 qubit.


Requisiti

Prima di iniziare questa procedura guidata, assicurati di aver installato quanto segue:

  • Qiskit SDK v2.1 o versioni successive, con supporto alla visualizzazione
  • Qiskit Runtime v0.40 o più tardi (pip install qiskit-ibm-runtime)

Configura

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

Esempio di simulatore su piccola scala

Salteremo questo passaggio poiché la mitigazione degli errori di runtime non è supportata sui simulatori.


Esempio di hardware

Fase 1: mappare gli input classici su un problema quantistico

Questa procedura dettagliata presuppone che il problema classico sia già stato mappato su quello quantistico. Inizia costruendo un circuito e un osservabile da misurare. Sebbene le tecniche qui utilizzate siano applicabili a molti tipi diversi di circuiti, per semplicità questa guida utilizza il efficient_su2 circuito incluso nella libreria di circuiti Qiskit.

efficient_su2 è un circuito quantistico parametrizzato progettato per essere eseguito in modo efficiente su hardware quantistico con connettività limitata dei qubit, pur rimanendo sufficientemente espressivo da risolvere problemi in ambiti applicativi quali l'ottimizzazione e la chimica. È costruito alternando strati di porte a singolo qubit parametrizzate con uno strato contenente uno schema fisso di porte a due qubit, per un numero prestabilito di ripetizioni. La configurazione delle porte a due qubit può essere specificata dall'utente. In questo caso è possibile utilizzare lo schema pairwise integrato, poiché riduce al minimo la profondità del circuito raggruppando le porte a due qubit nel modo più compatto possibile. Questo schema può essere realizzato utilizzando esclusivamente una 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:

Output of the previous code cell

Come grandezza osservabile, prendiamo l'operatore di Pauli ZZ che agisce sull'ultimo qubit, ZIIZ I \cdots I. Si noti che il fatto che l'ultimo qubit corrisponda al primo elemento di questa stringa è dovuto all'uso della notazione little-endian da parte di Qiskit.

# 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 con l'esecuzione del circuito e misurare la grandezza osservabile. Tuttavia, è anche necessario confrontare l'output del dispositivo quantistico con la risposta corretta, ovvero il valore teorico dell'osservabile, se il circuito fosse stato eseguito senza errori. Per i circuiti quantistici di piccole dimensioni è possibile calcolare questo valore simulando il circuito su un computer classico, ma ciò non è possibile per i circuiti più grandi, su scala industriale. È possibile ovviare a questo problema utilizzando la tecnica del "circuito speculare" (nota anche come "compute-uncompute"), utile per valutare le prestazioni dei dispositivi quantistici.

Circuito speculare

Nella tecnica del circuito a specchio, si concatena il circuito con il suo circuito inverso, che si forma invertendo ogni porta del circuito in ordine inverso. Il circuito risultante implementa l'operatore di identità, che può essere banalmente simulato. Poiché la struttura del circuito originale è conservata nel circuito speculare, l'esecuzione del circuito speculare dà comunque un'idea di come il dispositivo quantistico si comporterebbe sul circuito originale.

La seguente cella di codice assegna parametri casuali al circuito, quindi costruisce il circuito speculare utilizzando la unitary_overlap classe. Prima di duplicare il circuito, aggiungi un'istruzione di barriera per impedire al transpiler di unire le due parti del circuito situate ai lati della barriera, evitando così che il circuito transpilato risulti privo di porte logiche.

# 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:

Output of the previous code cell

Fase 2: Ottimizzazione del problema per l'esecuzione su hardware quantistico

È necessario ottimizzare il circuito prima di eseguirlo sull'hardware. Questo processo prevede alcune fasi:

  • Scegliete un layout di qubit che mappa i qubit virtuali del vostro circuito con i qubit fisici dell'hardware.
  • Inserire porte di scambio se necessario per instradare le interazioni tra qubit non collegati.
  • Tradurre le porte del circuito in istruzioni ISA (Instruction Set Architecture) che possono essere eseguite direttamente sull'hardware.
  • Eseguire ottimizzazioni del circuito per ridurre al minimo la profondità del circuito e il numero di porte.

Il transpiler integrato in Qiskit può eseguire tutti questi passaggi al posto tuo. Poiché questo esempio utilizza un circuito efficiente dal punto di vista hardware, il transpiler dovrebbe essere in grado di scegliere una disposizione dei qubit che non richieda l'inserimento di porte di scambio per il routing delle interazioni.

Prima di ottimizzare il circuito, devi scegliere il dispositivo hardware da utilizzare. 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
)
print(backend)

Output:

<IBMBackend('ibm_fez')>

È possibile traspilare il circuito sul backend desiderato creando un gestore di passaggi ed eseguendolo sul circuito. Un modo semplice per creare un gestore di pass è utilizzare la generate_preset_pass_manager funzione. Per una spiegazione più dettagliata della transpilazione con i gestori di passaggi, consultare la sezione "Transpile con i gestori di passaggi".

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:

Output of the previous code cell

Il circuito transpilato ora contiene solo istruzioni ISA. Tutte le porte sono state scomposte in porte di tipo " X\sqrt{X} ", rotazioni " RzR_z " e porte CZ.

Il processo di transpilazione ha mappato i qubit virtuali del circuito in qubit fisici sull'hardware. Le informazioni sulla disposizione dei qubit sono memorizzate nell'attributo layout del circuito transpilato. L'osservabile è stato definito anche in termini di qubit virtuali, quindi è necessario applicare questa disposizione all'osservabile, cosa che si può 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)

Output:

Original observable:
SparsePauliOp(['ZIIIIIIIII'],
              coeffs=[1.+0.j])

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

Passaggio 3: eseguire utilizzando Qiskit primitives

Ora si è pronti a eseguire il circuito utilizzando la primitiva Estimator.

Qui si invieranno cinque lavori distinti, iniziando senza alcuna soppressione o mitigazione degli errori e attivando successivamente le varie opzioni di soppressione e mitigazione degli errori disponibili nel servizio di calcolo di IBM Quantum. Per informazioni sulle opzioni, consultare le pagine seguenti:

Poiché questi processi possono essere eseguiti indipendentemente l'uno dall'altro, è possibile utilizzare la modalità batch per consentire a Quantum Compute di ottimizzare i tempi di esecuzione.

pub = (isa_circuit, isa_observable)

jobs = []

with Batch(backend=backend) as batch:
    estimator = Estimator(mode=batch)
    estimator.options.environment.job_tags = [
        "TUT_CEM_SS"
    ]  # add tag for this small scale job
    # 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)

Fase 4: Post-elaborazione e restituzione del risultato nel formato classico desiderato

Infine, è possibile analizzare i dati. Qui si recuperano i risultati del lavoro, si estraggono i valori di aspettativa misurati e si tracciano i valori, comprese le 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:

Output of the previous code cell

A questa scala ridotta, è difficile vedere l'effetto della maggior parte delle tecniche di attenuazione degli errori, ma l'estrapolazione a rumore zero offre un miglioramento notevole. Tuttavia, si noti che questo miglioramento non è gratuito, perché il risultato ZNE presenta anche una barra di errore più grande.


Esempio di hardware su larga scala

Quando si sviluppa un esperimento, è utile iniziare con un circuito di piccole dimensioni per facilitare la visualizzazione e la simulazione. Ora che avete sviluppato e testato il nostro flusso di lavoro su un circuito a 10 qubit, potete scalarlo fino a 50 qubit. La seguente cella di codice ripete tutti i passaggi di questa procedura dettagliata, 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)
    estimator.options.environment.job_tags = [
        "TUT_CEM_LS"
    ]  # add tag for this large scale job
    # 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:

Output of the previous code cell

Confrontando i risultati a 50-qubit con quelli a 10-qubit di prima, si può notare quanto segue (i risultati potrebbero differire da un'esecuzione all'altra):

  • Tutti gli esperimenti danno risultati più vicini al valore ideale e tutte le barre di errore sono più strette.
  • L'introduzione del disaccoppiamento dinamico potrebbe aver peggiorato le prestazioni rispetto al caso senza misure di mitigazione. Non c'è da stupirsi, dato che il circuito è molto fitto. Il disaccoppiamento dinamico è utile soprattutto quando nel circuito si verificano lunghi intervalli in cui i qubit rimangono inattivi senza che venga loro applicata alcuna porta logica. Quando tali intervalli non sono presenti, il disaccoppiamento dinamico non è efficace e può addirittura peggiorare le prestazioni a causa di errori negli stessi impulsi di disaccoppiamento dinamico. Il circuito da 10 qubit potrebbe essere stato troppo piccolo per permetterci di osservare questo effetto.
  • Con l'estrapolazione a rumore zero, il risultato è molto vicino al valore ideale. Questo dimostra la potenza di ZNE.

Passi successivi

Suggerimenti

Se questo lavoro ti è sembrato interessante, potrebbero interessarti i seguenti materiali su alcune tecniche aggiuntive di mitigazione e soppressione degli errori che non sono state menzionate in questo tutorial:

Questa pagina è stata utile?
Segnala un bug, un errore di battitura o richiedi contenuti su GitHub.