Skip to main content
IBM Quantum Platform

Retropropagazione dell'operatore (OBP) per la stima dei valori attesi

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


Risultati di apprendimento

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

  • Come utilizzare qiskit-addon-obp per ridurre la profondità del circuito quantistico a costo di un maggior numero di esecuzioni del circuito
  • Come utilizzare qiskit-addon-utils per costruire gli hamiltoniani XYZ e i relativi circuiti di evoluzione temporale

Prerequisiti

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

  • Utilizzo della primitiva Estimator per calcolare i valori attesi di una grandezza osservabile

Sfondo

La retropropagazione degli operatori è una tecnica che consiste nell'assorbire le operazioni dalla fine di un circuito quantistico nell'osservabile misurato, riducendo in genere la profondità del circuito a scapito dell'introduzione di termini aggiuntivi nell'osservabile. L'obiettivo è quello di applicare il backpropagation a una porzione del circuito il più ampia possibile, senza che la grandezza dell'osservabile diventi eccessiva. Nell'add-on OBP Qiskit è disponibile un'implementazione basata su Qiskit. Per ulteriori informazioni, consultare la documentazione corrispondente.

Si consideri un circuito di esempio in cui si deve misurare un osservabile O=PcPPO = \sum_P c_P P, dove PP sono i vettori di Pauli e cPc_P sono i coefficienti. Indichiamo il circuito come un unico " UU " unitario, che può essere suddiviso logicamente in " U=UCUQU = U_C U_Q ", come illustrato nella figura sottostante.

Schema del circuito che mostra Uq seguito da Uc

La retropropagazione dell'operatore incorpora l' UCU_C e unitaria nell'osservabile, facendola evolvere come O=UCOUC=PcPUCPUCO' = U_C^{\dagger}OU_C = \sum_P c_P U_C^{\dagger}PU_C. In altre parole, parte del calcolo viene eseguita in modo classico attraverso l'evoluzione dell'osservabile da OO a OO'. Il problema originale può ora essere riformulato come la misurazione dell'osservabile OO' per il nuovo circuito a profondità inferiore, il cui operatore unitario è UQU_Q.

L' UCU_C e unitario è rappresentato come un insieme di sezioni UC=USUS1...U2U1U_C = U_S U_{S-1}...U_2U_1. Esistono diversi modi per definire una sezione. Ad esempio, nel circuito sopra riportato, ogni strato di gate " RzzR_{zz} " e ogni strato di gate " RxR_x " può essere considerato come una singola sezione. La retropropagazione comporta, in linea di massima, il calcolo dell' O=Πs=1SPcPUsPUsO' = \Pi_{s=1}^S \sum_P c_P U_s^{\dagger} P U_s. Ogni sezione UsU_s può essere rappresentata come Us=exp(iθsPs2)U_s = exp(\frac{-i\theta_s P_s}{2}), dove PsP_s è un operatore di Pauli a -qubit nn e θs\theta_s è uno scalare. È facile verificare che

UsPUs=Pif [P,Ps]=0,U_s^{\dagger} P U_s = P \qquad \text{if} ~[P,P_s] = 0, UsPUs=cos(θs)P+isin(θs)PsPif {P,Ps}=0U_s^{\dagger} P U_s = \qquad cos(\theta_s)P + i sin(\theta_s)P_sP \qquad \text{if} ~\{P,P_s\} = 0

Nell'esempio sopra riportato, se {P,Ps}=0\{P,P_s\} = 0, allora per calcolare il valore atteso è necessario eseguire due circuiti quantistici, anziché uno solo. Pertanto, la retropropagazione potrebbe aumentare il numero di termini nell'osservabile, determinando un maggior numero di esecuzioni del circuito. Un modo per consentire una retropropagazione più profonda nel circuito, evitando al contempo che l'operatore diventi troppo grande, consiste nel troncare i termini con coefficienti piccoli, anziché aggiungerli all'operatore. Ad esempio, nell'esempio precedente, si potrebbe scegliere di troncare il termine che coinvolge PsPP_sP, a condizione che θs\theta_s sia sufficientemente piccolo. Il troncamento dei termini può comportare una riduzione del numero di circuiti quantistici da eseguire, ma ciò comporta un errore nel calcolo del valore atteso finale, proporzionale all'ampiezza dei coefficienti dei termini troncati.


Requisiti

Prima di iniziare questa esercitazione, assicuratevi di aver installato quanto segue:

  • Qiskit SDK v2.0 o versioni successive, con supporto alla visualizzazione
  • Qiskit Runtime v0.22 o versioni successive (pip install qiskit-ibm-runtime)
  • Componente aggiuntivo OBP Qiskit 0.3 o versioni successive (pip install qiskit-addon-obp)
  • Qiskit addon utils 0.3 o versioni successive (pip install qiskit-addon-utils)

Configura

import numpy as np
import matplotlib.pyplot as plt

from qiskit.primitives import StatevectorEstimator
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import CouplingMap
from qiskit.synthesis import LieTrotter

from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian
from qiskit_addon_utils.problem_generators import (
    generate_time_evolution_circuit,
)
from qiskit_addon_utils.slicing import slice_by_depth, combine_slices
from qiskit_addon_obp.utils.simplify import OperatorBudget
from qiskit_addon_obp import backpropagate
from qiskit_addon_obp.utils.truncating import setup_budget

from rustworkx.visualization import graphviz_draw

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorV2, EstimatorOptions

Esempio di simulatore su piccola scala

Questo tutorial implementa un modello Qiskit per la simulazione delle dinamiche quantistiche di una catena di spin di Heisenberg utilizzando l 'add-on OBP per Qiskit. Si noti che in un simulatore privo di rumore, il valore atteso ottenuto con e senza retropropagazione sarà lo stesso.

Fase 1: mappare gli input classici su un problema quantistico

Mappare l'evoluzione temporale di un modello quantistico di Heisenberg in un esperimento quantistico

Per prima cosa, useremo la generate_xyz_hamiltonian funzione di qiskit-addon-utils per generare un hamiltoniano di tipo Heisenberg su un dato grafo di connettività. Questo grafico può essere sia un grafico a barre ( rustworkx.PyGraph ) che un grafico a linee ( CouplingMap ). Di seguito utilizzeremo una catena CouplingMap lineare di 10 qubit.

num_qubits = 10
layout = [(i - 1, i) for i in range(1, num_qubits)]

# Instantiate a CouplingMap object
coupling_map = CouplingMap(layout)
graphviz_draw(coupling_map.graph, method="circo")

Output:

Output of the previous code cell

Successivamente, generiamo un operatore di Pauli che modella un hamiltoniano di Heisenberg XYZ:

H^XYZ=(j,k)E(Jxσjxσkx+Jyσjyσky+Jzσjzσkz)+jV(hxσjx+hyσjy+hzσjz),{\hat{\mathcal{H}}_{XYZ} = \sum_{(j,k)\in E} (J_{x} \sigma_j^{x} \sigma_{k}^{x} + J_{y} \sigma_j^{y} \sigma_{k}^{y} + J_{z} \sigma_j^{z} \sigma_{k}^{z}) + \sum_{j\in V} (h_{x} \sigma_j^{x} + h_{y} \sigma_j^{y} + h_{z} \sigma_j^{z}),}

dove G(V,E)G(V,E) è il grafico della mappa di accoppiamento. Per questo tutorial abbiamo impostato Jx,Jy,JzJ_x, J_y, J_z in modo che rimandi a π8,π4,π2\frac{\pi}{8}, \frac{\pi}{4}, \frac{\pi}{2} e hx,hy,hzh_x, h_y, h_z in modo che rimandi a π3,π6,π9\frac{\pi}{3}, \frac{\pi}{6}, \frac{\pi}{9}.

# Get a qubit operator describing the Heisenberg XYZ model
hamiltonian = generate_xyz_hamiltonian(
    coupling_map,
    coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),
    ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),
)
print(hamiltonian)

Output:

SparsePauliOp(['IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII', 'IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIYII', 'IIIIIIIZII', 'IIIIIIXIII', 'IIIIIIYIII', 'IIIIIIZIII', 'IIIIIXIIII', 'IIIIIYIIII', 'IIIIIZIIII', 'IIIIXIIIII', 'IIIIYIIIII', 'IIIIZIIIII', 'IIIXIIIIII', 'IIIYIIIIII', 'IIIZIIIIII', 'IIXIIIIIII', 'IIYIIIIIII', 'IIZIIIIIII', 'IXIIIIIIII', 'IYIIIIIIII', 'IZIIIIIIII', 'XIIIIIIIII', 'YIIIIIIIII', 'ZIIIIIIIII'],
              coeffs=[0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,
 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,
 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,
 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,
 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,
 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,
 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 1.04719755+0.j,
 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,
 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,
 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,
 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,
 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
 0.34906585+0.j])

A partire dall'operatore del qubit, possiamo generare un circuito quantistico che ne modella l'evoluzione temporale. Abbiamo utilizzato generate_time_evolution_circuit la scomposizione di Lie-Trotter per costruire il circuito di evoluzione temporale.

circuit = generate_time_evolution_circuit(
    hamiltonian,
    time=0.2,
    synthesis=LieTrotter(reps=2),
)
circuit.draw("mpl", style="iqp", fold=-1)

Output:

Output of the previous code cell

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

Creare sezioni di circuito per la retropropagazione

La backpropagate funzione esegue la retropropagazione di intere sezioni del circuito alla volta. Pertanto, la scelta della suddivisione in strati può influire sulle prestazioni della retropropagazione per un determinato problema. In questa sede raggrupperemo i gate dello stesso tipo in sezioni utilizzando la slice_by_depth funzione.

Per un approfondimento sul circuit slicing, dai un'occhiata a questa guida pratica del qiskit-addon-utils pacchetto.

slices = slice_by_depth(circuit, max_slice_depth=1)
print(f"Separated the circuit into {len(slices)} slices.")

Output:

Separated the circuit into 18 slices.

Limitare la crescita dell'operatore durante la retropropagazione

Durante la retropropagazione, il numero di termini nell'operatore tenderà generalmente ad avvicinarsi rapidamente a 2L2^L, dove LL è il numero di sezioni. Quando due termini dell'operatore non commutano a livello di qubit, occorrono circuiti distinti per ottenere i valori attesi a essi corrispondenti. Ad esempio, se abbiamo un osservabile a due qubit O=0.1XX+0.3IZ0.5IXO = 0.1 XX + 0.3 IZ - 0.5 IX, allora, dato che [XX,IX]=0[XX,IX] = 0, è sufficiente effettuare una misurazione su una singola base per calcolare i valori attesi di questi due termini. Tuttavia, l'operatore IZIZ è anticommutativo rispetto agli altri due termini, quindi occorre una misurazione di base separata per calcolare il valore atteso dell'operatore IZIZ. In altre parole, occorrono due circuiti anziché uno solo per calcolare l'operatore O\langle O \rangle. Man mano che il numero di termini nell'operatore aumenta, è possibile che aumenti anche il numero di esecuzioni del circuito richieste.

La dimensione dell'operatore può essere delimitata specificando il kwarg operator_budget della funzione backpropagate , che accetta un'istanza OperatorBudget istanza.

Per controllare la quantità di risorse aggiuntive (numero di iterazioni del circuito e, di conseguenza, il tempo richiesto dalla QPU) assegnate, limitiamo il numero massimo di gruppi di Pauli commutativi a livello di qubit che l'osservabile sottoposto a retropropagazione può avere. Qui specifichiamo che la retropropagazione deve interrompersi quando il numero di gruppi di Pauli commutativi a livello di qubit presenti nell'operatore supera gli otto.

op_budget = OperatorBudget(max_qwc_groups=8)

Retropropagare le sezioni dal circuito

Per prima cosa specifichiamo che l'osservabile è MZ=1Ni=1NZiM_Z = \frac{1}{N} \sum_{i=1}^N \langle Z_i \rangle, essendo NN il numero di qubit. Si retropropagheranno le fette del circuito di evoluzione temporale fino a quando i termini dell'osservabile non potranno più essere combinati in gruppi di Pauli commutativi di otto o meno qubit.

observable = SparsePauliOp.from_sparse_list(
    [("Z", [i], 1 / num_qubits) for i in range(num_qubits)],
    num_qubits=num_qubits,
)
observable

Output:

SparsePauliOp(['IIIIIIIIIZ', 'IIIIIIIIZI', 'IIIIIIIZII', 'IIIIIIZIII', 'IIIIIZIIII', 'IIIIZIIIII', 'IIIZIIIIII', 'IIZIIIIIII', 'IZIIIIIIII', 'ZIIIIIIIII'],
              coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,
 0.1+0.j, 0.1+0.j])

Come potete vedere qui sotto, abbiamo eseguito la retropropagazione su sei sezioni e i termini sono stati raggruppati in sei gruppi anziché in otto. Ciò implica che la retropropagazione di un'ulteriore sezione farebbe sì che il numero dei gruppi di Pauli superasse gli otto. Possiamo verificare che sia così esaminando i metadati restituiti. Si noti inoltre che in questa parte la trasformazione del circuito è esatta. In altre parole, nessun termine della nuova grandezza osservabile OO’ è stato troncato. Il circuito con retropropagazione e l'operatore con retropropagazione forniscono lo stesso risultato esatto del circuito e dell'operatore originali.

# Backpropagate slices onto the observable
bp_obs, remaining_slices, metadata = backpropagate(
    observable, slices, operator_budget=op_budget
)
# Recombine the slices remaining after backpropagation
bp_circuit = combine_slices(remaining_slices)

print(f"Backpropagated {metadata.num_backpropagated_slices} slices.")
print(
    f"New observable has {len(bp_obs.paulis)} terms, which can be combined into "
    f"{len(bp_obs.group_commuting(qubit_wise=True))} groups."
)
print(
    f"Note that backpropagating one more slice would result in "
    f"{metadata.backpropagation_history[-1].num_paulis[0]} terms "
    f"across {metadata.backpropagation_history[-1].num_qwc_groups} groups."
)
print("The remaining circuit after backpropagation looks as follows:")
bp_circuit.draw("mpl", fold=-1, scale=0.6)

Output:

Backpropagated 6 slices.
New observable has 60 terms, which can be combined into 6 groups.
Note that backpropagating one more slice would result in 114 terms across 12 groups.
The remaining circuit after backpropagation looks as follows:
Output of the previous code cell

Per l'esempio su piccola scala eseguito su un simulatore, non ricorreremo al troncamento. Questo perché, in assenza di rumore, il circuito con e senza retropropagazione porta allo stesso risultato, mentre il troncamento peggiora il risultato a causa dell'approssimazione aggiuntiva.

Trasformare i circuiti nel set di porte di base

Ora trasponiamo sia il circuito originale che quello ottenuto tramite backpropagation nel gate di base del backend. Non è necessario eseguire la transpilazione sul backend vero e proprio, dato che per l'istanza di piccole dimensioni utilizzeremo un simulatore.

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

Output:

<IBMBackend('ibm_kingston')>
pm_basis = generate_preset_pass_manager(
    optimization_level=3, basis_gates=backend.configuration().basis_gates
)
isa_circuit = pm_basis.run(circuit)
isa_bp_circuit = pm_basis.run(bp_circuit)

Passaggio 3: eseguire utilizzando Qiskit primitives

Per prima cosa, creiamo due blocchi unificati primitivi (PUB) corrispondenti al circuito originale e al circuito ottenuto tramite retropropagazione. Quindi eseguiamo le simulazioni su un Estimator ideale per ottenere i valori attesi.

pubs = [(isa_circuit, observable), (isa_bp_circuit, bp_obs)]
rng = np.random.default_rng()
estimator = StatevectorEstimator(seed=rng)
job = estimator.run(pubs)

Fase 4: Elaborazione finale e restituzione del risultato nel formato classico desiderato

Ora calcoliamo i valori attesi dei circuiti originali e di quelli sottoposti a retropropagazione.

primitive_result = job.result()
circuit_expval = primitive_result[0].data.evs.item()
bp_circuit_expval = primitive_result[1].data.evs.item()
methods = [
    "No backpropagation",
    "Backpropagation",
]
values = [circuit_expval, bp_circuit_expval]

ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
ax.set_ylim([0.6, 0.92])
ax.set_ylabel(r"$M_Z$", fontsize=12)

Output:

Text(0, 0.5, '$M_Z$')
Output of the previous code cell

Come previsto, i due valori attesi coincidono. Poiché stiamo utilizzando un simulatore di vettori di stato privo di rumore, la retropropagazione costituisce una trasformazione esatta della coppia circuito-osservabile; pertanto, il flusso di lavoro originale e quello ottenuto tramite retropropagazione devono produrre lo stesso valore dell' MZM_Z e. Il vantaggio della retropropagazione diventa evidente solo su hardware soggetto a rumore, dove il circuito retropropagato, essendo più breve, accumula meno errori, come illustrato nell'esempio di hardware su larga scala riportato di seguito.


Esempio di hardware su larga scala

Quando si progetta un esperimento, è utile partire da un circuito semplice per facilitare la creazione di rappresentazioni grafiche e simulazioni. Ora esaminiamo la retropropagazione degli operatori per un hamiltoniano di Heisenberg a 50 qubit con lo stesso insieme di valori per i parametri JJ e hh e lo stesso osservabile MZM_Z, ma per quattro passi di Trotter. Il valore atteso ideale a questa scala non può essere calcolato con un metodo di forza bruta, pertanto utilizziamo una rete tensoriale e otteniamo che il valore atteso ideale è pari a 0.89\simeq 0.89.

Oltre alla retropropagazione, in questo esempio su larga scala introduciamo anche la retropropagazione con troncamento. L'ideale sarebbe eseguire la retropropagazione il più possibile per ridurre la profondità del circuito effettivo. Tuttavia, ciò comporta spesso la presenza di un gran numero di termini non commutativi nell'osservabile aggiornato, aumentando il sovraccarico quantistico. Pertanto, possiamo eliminare i termini osservabili con coefficienti piccoli ricorrendo a una tecnica chiamata troncamento. Sebbene il troncamento consenta una maggiore propagazione riducendo il numero di termini nell'osservabile aggiornato, introduce anche una certa approssimazione. È quindi necessario limitare il troncamento entro certi limiti, in modo che l'errore di approssimazione non annulli la riduzione del rumore ottenuta grazie a una retropropagazione più profonda.

Per limitare l'entità del troncamento, assegniamo un margine di errore a ciascuna sezione, oltre al margine di errore complessivo per l'intero circuito sottoposto a retropropagazione, utilizzando la setup_budget funzione. Ciò garantisce che il troncamento sia controllato sia per ogni singola sezione che per l'intero circuito. Consulta anche questa guida per scoprire altri modi di ripartire il budget.

num_qubits = 50
layout = [(i - 1, i) for i in range(1, num_qubits)]

# Instantiate a CouplingMap object
coupling_map = CouplingMap(layout)

hamiltonian = generate_xyz_hamiltonian(
    coupling_map,
    coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),
    ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),
)

# Generate a time evolution circuit for the Hamiltonian
circuit = generate_time_evolution_circuit(
    hamiltonian,
    time=0.2,
    synthesis=LieTrotter(reps=4),
)

# Define the observable to measure
observable = SparsePauliOp.from_sparse_list(
    [("Z", [i], 1 / num_qubits) for i in range(num_qubits)],
    num_qubits,
)

slices = slice_by_depth(circuit, max_slice_depth=1)

# Define the maximum number of qwc groups allowed in the
# backpropagated observable,
# and the truncation error budget
op_budget = OperatorBudget(max_qwc_groups=15)
truncation_error_budget = setup_budget(
    max_error_total=0.03, max_error_per_slice=0.005
)

# First backpropagation without truncation
bp_obs, remaining_slices, metadata = backpropagate(
    observable, slices, operator_budget=op_budget
)
bp_circuit = combine_slices(remaining_slices)

# Now backpropagate with truncation, using the same operator budget and
# the defined truncation error budget
bp_obs_trunc, remaining_slices_trunc, metadata = backpropagate(
    observable,
    slices,
    operator_budget=op_budget,
    truncation_error_budget=truncation_error_budget,
)
bp_circuit_trunc = combine_slices(
    remaining_slices_trunc, include_barriers=False
)

# Now we transpile the original circuit and the two backpropagated circuits,
# and apply the layout to the corresponding observables
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)

isa_circuit = pm.run(circuit)
isa_bp_circuit = pm.run(bp_circuit)
isa_bp_circuit_trunc = pm.run(bp_circuit_trunc)

isa_observable = observable.apply_layout(isa_circuit.layout)
isa_bp_observable = bp_obs.apply_layout(isa_bp_circuit.layout)
isa_bp_observable_trunc = bp_obs_trunc.apply_layout(
    isa_bp_circuit_trunc.layout
)

# Compare the 2-qubit depth of each transpiled circuit to see how much
# depth backpropagation saved
print(
    f"2-qubit depth without backpropagation: "
    f"{isa_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
    f"2-qubit depth with backpropagation: "
    f"{isa_bp_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
    f"2-qubit depth with backpropagation and truncation: "
    f"{isa_bp_circuit_trunc.depth(lambda x: x.operation.num_qubits == 2)}"
)

pubs = [
    (isa_circuit, isa_observable),
    (isa_bp_circuit, isa_bp_observable),
    (isa_bp_circuit_trunc, isa_bp_observable_trunc),
]

# Now we instantiate the Estimator primitive for the hardware with
# ZNE and measurement error
# mitigation and compute the three circuits and observables
options = EstimatorOptions()
options.default_precision = 0.01
options.resilience_level = 2
options.resilience.zne.noise_factors = [1, 1.2, 1.4]
options.resilience.zne.extrapolator = ["linear"]
estimator = EstimatorV2(mode=backend, options=options)

estimator.options.environment.job_tags = ["TUT_OBP"]
job = estimator.run(pubs)

# Retrieve the results and the standard deviations
result_no_bp = job.result()[0].data.evs.item()
result_bp = job.result()[1].data.evs.item()
result_bp_trunc = job.result()[2].data.evs.item()

std_no_bp = job.result()[0].data.stds.item()
std_bp = job.result()[1].data.stds.item()
std_bp_trunc = job.result()[2].data.stds.item()

Output:

2-qubit depth without backpropagation: 24
2-qubit depth with backpropagation: 20
2-qubit depth with backpropagation and truncation: 18
print(f"Expectation value without backpropagation: {result_no_bp}")
print(f"Backpropagated expectation value: {result_bp}")
print(f"Backpropagated expectation value with truncation: {result_bp_trunc}")

Output:

Expectation value without backpropagation: 0.9543907942381811
Backpropagated expectation value: 0.9445337385406468
Backpropagated expectation value with truncation: 0.934050286970965
# Plot the results
methods = [
    "No backpropagation",
    "Backpropagation",
    "Backpropagation w/ truncation",
]
values = [result_no_bp, result_bp, result_bp_trunc]
error_bars = [std_no_bp, std_bp, std_bp_trunc]

ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.errorbar(methods, values, yerr=error_bars, fmt="o", color="r", capsize=5)
plt.axhline(0.89)
ax.set_ylim([0.8, 0.98])
plt.text(0.25, 0.895, "Exact result")
ax.set_ylabel(r"$M_Z$", fontsize=12)

Output:

Text(0, 0.5, '$M_Z$')
Output of the previous code cell

Passi successivi

Se questo lavoro ti è sembrato interessante, potrebbero interessarti anche i seguenti contenuti:

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