Troncare i termini di Pauli durante la retropropagazione
Questa guida spiega come configurare il meccanismo di troncamento dei termini di Pauli fornito dal modulo qiskit_addon_obp.utils.truncating.
La retropropagazione dell'operatore (OBP) può essere usata per ridurre la profondità dei circuiti quantistici al costo di un'osservabile più complessa. Per ottenere risultati significativi dall'OBP, di solito è necessario troncare dei termini dalla propria osservabile per evitare che cresca troppo. Un modo per consentire una retropropagazione più profonda nel circuito, evitando al contempo che l'operatore cresca troppo, è troncare i termini con coefficienti piccoli anziché aggiungerli all'operatore. Troncare i termini può comportare meno circuiti quantistici da eseguire, ma ciò introduce un certo errore nel calcolo finale del valore di aspettazione, proporzionale alla magnitudine dei coefficienti dei termini troncati.
Il metodo backpropagate accetta un TruncationErrorBudget opzionale, che configura il troncamento dei termini di Pauli a basso peso per ciascuna osservabile dopo la retropropagazione riuscita di ogni slice. La quantità di termini troncati dipende da vari parametri di configurazione specificati dall'utente. Al momento è disponibile una sola strategia di troncamento: il metodo truncate_binary_search. Dati un'osservabile e un certo budget, esegue una ricerca binaria sui termini di Pauli e sui coefficienti all'interno di quell'osservabile per trovare la soglia ottimale, tale che la somma dei coefficienti troncati sia massima ma inferiore al budget.
Nota: per impostazione predefinita, la norma L1 viene usata per valutare e limitare l'errore di troncamento; tuttavia, l'impostazione p_norm consente di specificare la norma Lp da usare.
Per maggiori informazioni su come usare questa impostazione, consulta la guida Use different Lp-norms for Pauli term truncation.
Gli esempi seguenti illustrano vari modi di costruire un TruncationErrorBudget usando la funzione associata setup_budget.
Costruire un circuito di esempio
Questa guida usa le seguenti slice del circuito:
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "qiskit": "qiskit", "qiskit_addon_obp": "qiskit-addon-obp", "qiskit_addon_utils": "qiskit-addon-utils", "rustworkx": "rustworkx"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import rustworkx.generators
from qiskit.synthesis import LieTrotter
from qiskit_addon_utils.problem_generators import (
PauliOrderStrategy,
generate_time_evolution_circuit,
generate_xyz_hamiltonian,
)
from qiskit_addon_utils.slicing import combine_slices, slice_by_gate_types
# Generate a linear chain of 10 qubits
linear_chain = rustworkx.generators.path_graph(10)
# Use an arbitrary XY model
hamiltonian = generate_xyz_hamiltonian(
linear_chain,
coupling_constants=(0.05, 0.02, 0.0),
ext_magnetic_field=(0.02, 0.08, 0.0),
pauli_order_strategy=PauliOrderStrategy.InteractionThenColor,
)
# Evolve for some time
circuit = generate_time_evolution_circuit(
hamiltonian, synthesis=LieTrotter(reps=3), time=2.0
)
# slice the circuit by gate type
slices = slice_by_gate_types(circuit)
# for visualization purposes, recombine the slices with barriers between them and draw the resulting circuit
combine_slices(slices, include_barriers=True).draw("mpl", fold=50, scale=0.6)

Esamineremo una singola osservabile semplice:
from qiskit.quantum_info import SparsePauliOp
obs = SparsePauliOp("IIIIIZIIII")
Il caso più semplice: un budget di troncamento fisso per ogni slice
Il budget disponibile per il troncamento dei termini di Pauli può variare a ogni passo della retropropagazione. Per capire come funziona, esaminiamo prima il caso più semplice di un budget di troncamento fisso, come specificato dall'utente.
Il modo più diretto per specificare il budget di troncamento è usare l'argomento max_error_per_slice.
In effetti, è ciò che viene fatto nel tutorial Reducing depth of circuits with operator backpropagation. Impostare max_error_per_slice su un float fa sì che a ogni slice venga assegnato un budget pari a quel valore.
Nell'esempio seguente impostiamo questo valore a 0.001, il che garantisce un errore di troncamento implicito di al più 0.018, se tutte le 18 slice fossero retropropagate.
Nota che qualsiasi budget di errore rimanente dopo aver retropropagato una slice e troncato i termini con coefficienti piccoli verrà sempre aggiunto al budget di errore della slice successiva.
from qiskit_addon_obp.utils.truncating import setup_budget
truncation_error_budget = setup_budget(max_error_per_slice=0.001)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.001], max_error_total=inf, p_norm=1)
from qiskit_addon_obp import backpropagate
from qiskit_addon_obp.utils.simplify import OperatorBudget
op_budget = OperatorBudget(max_qwc_groups=10)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
New observable contains 29 terms and 10 commuting groups.
Usiamo ora l'istanza OBPMetadata e gli strumenti forniti dal modulo visualization per visualizzare il processo di retropropagazione.
-
Il grafico in alto a sinistra mostra che abbiamo budget sufficiente per iniziare a troncare i termini dell'osservabile dopo aver retropropagato la terza slice. A partire dalla terza slice, sappiamo che troncheremo almeno un termine da ogni slice che retropropaghiamo, perché dopo ogni slice introduciamo un certo errore di troncamento.
-
Il grafico in alto a destra mostra che il budget di errore cresce fino a
.003per la terza slice. Vediamo un brusco calo del budget rimanente, il che significa che dei termini sono stati troncati dall'osservabile. Questo è coerente con quanto dedotto dal grafico in alto a sinistra. -
Il grafico in basso a sinistra mostra che, man mano che tronchiamo termini dalla nostra osservabile, l'errore complessivo accumulato cresce in modo monotono. Questo grafico conferma anche che nessun termine è stato troncato fino a dopo la retropropagazione della terza slice.
-
Il grafico in basso a destra mostra che il numero di gruppi di Pauli commutanti nella nostra osservabile è cresciuto fino al limite specificato di
10. Questo grafico mostra anche come retropropagare un ulteriore layer farebbe superare all'osservabile il limite specificato, come si vede dall'incrocio tra le linee nera e rossa.
Nota che in tutti questi grafici l'asse x enumera le slice retropropagate, ma poiché l'OBP lavora sulla fine del circuito, slice 1 è l'ultima slice in assoluto, slice 2 è quella precedente, e così via.
from matplotlib import pyplot as plt
from qiskit_addon_obp.utils.visualization import (
plot_accumulated_error,
plot_left_over_error_budget,
plot_num_qwc_groups,
plot_slice_errors,
)
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Specificare esplicitamente il budget per slice
Se sai come assegnare un budget a ciascuna slice in modo da ottimizzare le prestazioni della retropropagazione, potresti voler assegnare esplicitamente un budget a ogni slice. A scopo dimostrativo, assegneremo budget zero alle prime tre slice e useremo un budget di .001 per slice per le restanti.
# Zero out the first 3 slices' budgets
max_error_per_slice = [0.0] * 3 + [0.001] * (len(slices) - 3)
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001], max_error_total=inf, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
New observable contains 32 terms and 10 commuting groups.
Rimuovere il budget dai primi tre layer ha fatto sì che nessun termine venisse troncato fino a dopo il quarto layer, come si può confermare in tre di questi grafici. Ciò che può sorprendere un po' è che, sebbene alla quarta slice non sia stato passato alcun budget residuo, un termine è stato comunque troncato usando il budget assegnato di .001. Questo è evidente dal grafico in alto a sinistra, ma si osserva anche nel grafico in alto a destra, dove la curva del budget residuo si appiattisce tra le slice tre e quattro. Un altro dettaglio notevole è che da questo punto in poi almeno un termine è stato troncato, come nell'esempio precedente.
Il punto chiave è che, sebbene nel secondo esempio abbiamo introdotto meno errore di troncamento azzerando i budget di alcune slice, siamo riusciti a retropropagare lo stesso numero di slice e la nostra osservabile contiene lo stesso numero di gruppi di Pauli commutanti. Possiamo confermare che il limite sul nostro errore è più piccolo nel secondo esempio ispezionando il grafico in basso a sinistra.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Specificare il budget in modo ciclico
Se hai un circuito con un certo schema ripetibile, come un circuito di Trotter, potresti voler specificare un budget per quel sottoinsieme ripetuto di slice e far usare quel budget per tutte le successive ripetizioni di quelle slice.
Più concretamente, il circuito di esempio che stiamo usando ha sei slice ripetute tre volte, per un totale di 18 slice. Assegneremo arbitrariamente budget zero ai layer a singolo qubit e RYY, e un budget di .003 a ciascuno dei layer RXX. Osserveremo come specificare il budget come sequenza di lunghezza 6 faccia sì che il budget venga applicato ciclicamente a tutte le 18 slice.
Di nuovo, nota che le slice vengono retropropagate in ordine inverso (cioè a partire dalla fine). Pertanto, la prima voce del nostro budget ciclico viene in realtà usata per l'ultima slice, la seconda voce per la slice precedente, e così via.
# Specify a length-6 per-slice budget.
# This will be cycled over three times to be applied to the 18 slices
max_error_per_slice = [0.0] * 4 + [0.003] * 2
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.0, 0.003, 0.003], max_error_total=inf, p_norm=1)
op_budget = OperatorBudget(max_qwc_groups=20)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 13 circuit slices.
New observable contains 49 terms and 14 commuting groups.
Come si vede nelle immagini in alto a sinistra e in basso a sinistra, non è stato eseguito alcun troncamento sulle prime quattro slice, poiché a esse non era stato assegnato alcun budget di errore. Le slice cinque e sei hanno avuto alcuni termini troncati man mano che il budget diventava disponibile.
Come si vede nell'immagine in alto a sinistra, dopo aver retropropagato la slice sette è stata introdotta una quantità relativamente grande di errore, anche se a quella slice non era stato assegnato alcun budget. Questo accade perché è stato usato solo circa .002 del budget totale di .006 assegnato alle slice cinque e sei, quindi il resto è stato inoltrato alla slice sette e per lo più consumato, come si vede nell'immagine in alto a destra.
Le immagini in alto a sinistra e in alto a destra mostrano che la piccola quantità di budget residuo viene esaurita tra le slice 8-10 e che nuovo budget diventa disponibile alla slice 11, come previsto. Le immagini in alto a sinistra, in alto a destra e in basso a sinistra dimostrano tutte il comportamento ciclico dell'argomento max_error_per_slice quando la sua lunghezza è inferiore al numero di slice. Questo comportamento ciclico sarebbe proseguito per tutte le slice, ma il criterio di arresto max_qwc_groups è stato raggiunto dopo aver retropropagato 13 slice, come si vede nell'immagine in basso a destra.
È interessante anche che il numero di gruppi di Pauli in realtà diminuisca dopo che il secondo round di budget diventa disponibile alla slice 11. Questo perché c'erano gruppi con coefficienti piccoli che non potevano essere troncati finché non c'è stato più budget dopo la retropropagazione della slice 11, quindi si sono accumulati nell'osservabile per diverse iterazioni. Questo caso evidenzia anche che max_qwc_groups deve essere superato affinché l'algoritmo termini.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Limitare l'errore totale
Oltre a specificare il budget di errore per slice, si può specificare la quantità massima di errore da introdurre con il troncamento. Una volta raggiunto quel limite, non verrà eseguito altro troncamento; tuttavia, la retropropagazione continuerà finché l'osservabile non diventa troppo grande e viene soddisfatto uno dei criteri di arresto.
Rieseguiamo ora l'esperimento precedente, impostando un limite su max_error_total in modo che il budget di errore venga esaurito dopo aver retropropagato la slice sette.
# Specify a length-6 per-slice budget.
# This will be cycled over 3 times to be applied to the 18 slices
max_error_per_slice = [0.0] * 4 + [0.003] * 2
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice, max_error_total=0.006
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.0, 0.003, 0.003], max_error_total=0.006, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 10 circuit slices.
New observable contains 67 terms and 20 commuting groups.
Come previsto, il nostro errore di troncamento raggiunge il tetto di .006 (grafico in basso a sinistra). È notevole che in questa esecuzione non siamo riusciti a retropropagare la slice 11. Questo perché non avevamo budget sufficiente per troncare i termini e il numero di gruppi di Pauli commutanti è cresciuto oltre il limite di 20, come si vede nell'immagine in basso a destra.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Può essere desiderabile limitare semplicemente il budget complessivo con max_error_total senza specificare max_error_per_slice.
truncation_error_budget = setup_budget(max_error_total=0.018)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.018], max_error_total=0.018, p_norm=1)
L'output della cella precedente può sorprendere un po', perché il per_slice_budget è impostato a max_error_total. Questo indica che l'intero budget disponibile verrà consumato in modo greedy.
Puoi pensarla così: l'intero budget è disponibile per ogni slice (perché cicliamo sul per_slice_budget). Ma qualsiasi budget già consumato verrà dedotto dal budget disponibile in quel dato punto dell'algoritmo.
op_budget = OperatorBudget(max_qwc_groups=10)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 9 circuit slices.
New observable contains 25 terms and 9 commuting groups.
L'immagine in alto a destra mostra come l'intero budget di errore di .018 venga reso disponibile per la prima slice. Non viene eseguito alcun troncamento fino alla terza slice, quindi il budget residuo rimane costante. Il budget diminuisce in modo monotono, poiché l'intero budget è reso disponibile a ogni slice retropropagata finché non viene esaurito.
È notevole che questo esperimento abbia prodotto due slice retropropagate in meno rispetto al primo esperimento di questo notebook, che è quasi identico. Questo dimostra che per alcuni problemi distribuire il budget in modo uniforme può essere ottimale. Per altri problemi, permettere alle slice di consumare in modo greedy l'intero budget può dare prestazioni migliori.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Limitare insieme il numero di slice retropropagate e l'errore totale
Se non vuoi distribuire il tuo budget di errore lungo tutto il circuito ma non vuoi nemmeno consumarlo in modo greedy, puoi specificare il numero di slice che intendi retropropagare (num_slices), insieme a un budget di errore totale (max_error_total). Questo distribuirà il budget di errore in modo uniforme (secondo p_norm) tra le slice in input.
Qui limiteremo a 12 il numero di slice che potremmo retropropagare e manterremo lo stesso budget di errore totale.
num_slices = 12
truncation_error_budget = setup_budget(
max_error_total=0.018, num_slices=num_slices, p_norm=1
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0014999999999999998], max_error_total=0.018, p_norm=1)
Ora tenteremo di retropropagare le 12 slice per le quali abbiamo allocato un po' di budget nel passo precedente. Lo facciamo passando semplicemente le ultime 12 slice del nostro circuito. Con p_norm=1, ciascuna delle 12 slice ha un budget disponibile di 0.018 / num_slices = 0.0015.
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices[-num_slices:],
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
Poiché abbiamo passato a backpropagate un sottoinsieme delle nostre slice (nel codice slices[-num_slices:]), dobbiamo combinare le slice rimaste dopo la retropropagazione con le slice che non sono mai state inviate alla retropropagazione (nel codice slices[:-num_slices]).
Una volta combinate tutte le slice rimanenti, possiamo usare combine_slices per generare il QuantumCircuit a profondità ridotta. Successivamente, ispezioniamo quante slice sono state retropropagate rispetto a quanto è diventata grande la nostra osservabile.
# Recombine the slices remaining after backprop with the rest of the original circuit
reduced_circuit = combine_slices(slices[:-num_slices] + remaining_slices)
print(f"Backpropagated {num_slices - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 12 circuit slices.
New observable contains 29 terms and 9 commuting groups.
I grafici mostrano che abbiamo effettuato con successo la backpropagation di tutte le 12 slice mantenendo la nostra osservabile sotto i 10 gruppi di Pauli commutanti. Possiamo anche vedere che l'uso di num_slices insieme a max_error_total produce una distribuzione dei budget tra le slice, e il budget non utilizzato viene di nuovo inoltrato alla slice successiva. Questo è più evidente nel grafico in alto a destra, poiché il budget viene sia speso sia ricostituito durante la backpropagation.
È notevole che questo metodo di distribuzione dell'errore abbia prodotto i risultati migliori (più slice sottoposte a backpropagation), rispetto al primo esperimento di questo notebook e all'esempio immediatamente precedente. Tutti questi esempi assegnavano un budget di errore di .018, ma la backpropagation si è comportata in modo diverso a seconda di come quel budget è stato distribuito.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Lavorare con più osservabili
Il metodo qiskit_addon_obp.backpropagate permette di passare una sequenza di osservabili. Questo semplifica il flusso di lavoro quando si hanno più osservabili target.
Lo menzioniamo esplicitamente qui per mostrarti come la strategia di troncamento gestisce un caso simile. Ai fini di questo esempio, aggiungiamo un'osservabile extra a quella che abbiamo usato finora:
obs = [SparsePauliOp("IIIIIZIIII"), SparsePauliOp("IIIIIXIIII")]
Infine, ripetiamo il primo esperimento di questo tutorial, ma con due osservabili in cui eseguire la backpropagation del circuito.
In questo esempio, ciò non influisce sul numero di slice di cui è stato possibile eseguire la backpropagation. Tuttavia, possiamo vedere che le due osservabili hanno prodotto numeri diversi di termini di Pauli e di gruppi commutanti.
truncation_error_budget = setup_budget(max_error_per_slice=0.001)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.001], max_error_total=inf, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"The new first observable contains {len(bp_obs[0])} terms and {len(bp_obs[0].group_commuting(qubit_wise=True))} commuting groups."
)
print(
f"The new second observable contains {len(bp_obs[1])} terms and {len(bp_obs[1].group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
The new first observable contains 29 terms and 10 commuting groups.
The new second observable contains 23 terms and 8 commuting groups.
I grafici seguenti aiutano a illustrare come l'algoritmo di backpropagation gestisce più osservabili.
Innanzitutto, i grafici in alto a sinistra, in alto a destra e in basso a sinistra mostrano che il budget per troncare i termini è impostato individualmente per ciascuna osservabile. In altre parole, entrambe le osservabili hanno la possibilità di troncare termini assumendo un errore di 0.001 per slice sottoposta a backpropagation.
A causa della diversa natura delle osservabili, ciò comporta un consumo diverso del budget. In questo esempio, mostrano una grande sovrapposizione, cosa che non accade sempre.
Il grafico in basso a destra illustra che max_qwc_groups tiene conto di tutte le osservabili. Ciò significa che i termini di tutte le osservabili vengono raggruppati per ottenere un unico numero finale di gruppi commutanti qubit-wise, che viene confrontato con max_qwc_paulis. Lo stesso viene fatto per la soglia max_paulis (non trattata in questo notebook), che permette di impostare un limite al numero di termini di Pauli in tutte le osservabili.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])
