Simulare classicamente i circuiti con l'OBP
In questa guida imparerai a simulare classicamente istanze di QuantumCircuit per stimare valori di aspettazione interamente tramite l'OBP.
Poiché l'OBP prende un'osservabile e la retropropaga attraverso un circuito dato, la "simulazione" di un circuito equivale a calcolare il valore di aspettazione dell'osservabile target rispetto a questo circuito.
Come vedrai più avanti, il pacchetto qiskit-addon-obp è persino in grado di gestire semplici modelli di rumore, permettendoti di calcolare anche valori di aspettazione rumorosi!
Costruire un circuito di esempio
Ai fini di questa guida, useremo lo stesso circuito di esempio della guida al troncamento dei termini di Pauli:
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_obp": "qiskit-addon-obp", "qiskit_addon_utils": "qiskit-addon-utils", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "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
# we generate a linear chain of 10 qubits
num_qubits = 10
linear_chain = rustworkx.generators.path_graph(num_qubits)
# we 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,
)
# we 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)
Tuttavia, quanto sopra è semplicemente il circuito che descrive l'evoluzione temporale sotto un Hamiltoniano scelto. Ci serve anche uno stato iniziale da cui partire, rispetto al quale calcolare i valori di aspettazione della nostra osservabile.
Naturalmente potremmo scegliere come stato iniziale lo stato tutto zero (o vuoto), ma per mostrare come inserire un proprio stato iniziale, ne scegliamo uno diverso qui sotto.
Una possibilità sarebbe anteporre lo stato iniziale al nostro circuito di evoluzione temporale visto sopra: circuit.compose(initial_state, front=True).
Ma dato che abbiamo già suddiviso in slice il nostro circuit, è più semplice inserire lo stato iniziale come prima slice, cosa che facciamo qui sotto.
In questo modo possiamo semplicemente sostituire la prima slice con un altro stato iniziale, se in futuro vorremo cambiarlo, senza dover ricalcolare le nostre slice.
from qiskit.circuit import QuantumCircuit
initial_state = QuantumCircuit(num_qubits)
for i in range(0, num_qubits, 2):
initial_state.x(i)
slices.insert(0, initial_state)
# for visualization purposes only, we 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)

Simulare un valore di aspettazione senza rumore
Come osservabile target, scegliamo l'osservabile ZZ sui qubit centrali:
from qiskit.quantum_info import SparsePauliOp
obs = SparsePauliOp("IIIIZZIIII")
A questo punto siamo già pronti a simulare classicamente il valore di aspettazione usando l'OBP.
Per farlo, forniamo semplicemente tutte le slice al metodo backpropagate, in questo modo:
from qiskit_addon_obp import backpropagate
vacuum_state_obs, _, metadata = backpropagate(obs, slices)
Abbiamo ora retropropagato la nostra osservabile target obs attraverso l'intero circuito (incluso lo initial_state che abbiamo posto in slices[0]), ottenendo un nuovo SparsePauliOp il cui valore di aspettazione si ottiene proiettandolo sullo stato vuoto (|00...00>).
Questo si può ottenere in modo diretto sommando i coefficienti di tutti i termini di Pauli definiti nella base computazionale:
vacuum_state_obs.coeffs[~vacuum_state_obs.paulis.x.any(axis=1)].sum()
np.complex128(-0.8285688012239535+4.9487770271457865e-20j)
Come verifica di coerenza (e per dimostrare che funziona) possiamo confrontare il nostro risultato con lo Statevector di Qiskit:
from qiskit.quantum_info import Statevector
Statevector(combine_slices(slices)).expectation_value(obs)
np.complex128(-0.8285687255430366+0j)
Alcune note sulle prestazioni
L'efficienza computazionale della chiamata a backpropagate vista sopra dipenderà molto da diversi fattori, tra cui:
- la struttura del
circuit - il metodo di suddivisione del circuito in slice
- l'osservabile target
- i parametri di troncamento
Poiché il metodo backpropagate semplifica l'osservabile dopo che ogni slice è stata applicata, il numero di gate in una slice può influenzare drasticamente il carico computazionale.
La strategia più aggressiva in termini di semplificazione dell'operatore si ottiene suddividendo il circuito in slice di singole gate.
Inoltre, puoi sfruttare tutti i meccanismi di troncamento integrati nel metodo backpropagate.
Abbiamo scelto di non farlo sopra, ottenendo di fatto un valore di aspettazione esatto, ma puoi imparare come farlo nella guida al troncamento dei termini di Pauli.
Simulare un valore di aspettazione rumoroso
Il pacchetto qiskit-addon-obp supporta anche la gestione di modelli di rumore sotto forma di PauliLindbladError.
Questo è particolarmente utile quando hai caratterizzato il modello di rumore dei layer a 2 qubit del tuo circuito, per esempio usando il NoiseLearner.
In questa sezione vedrai come usare gli oggetti LayerError restituiti dal NoiseLearner per calcolare valori di aspettazione rumorosi usando l'OBP.
Ottenere un modello di rumore
Normalmente eseguiresti il NoiseLearner per ottenere un modello di rumore del tuo circuito specifico.
Per evitare complessità (e casualità) in questo tutorial, non lo faremo e inseriremo invece nel codice un modello di rumore per il nostro circuito.
Tuttavia, ci assicuriamo che la struttura dei nostri dati corrisponda a quella del NoiseLearnerResult.
Nella sua forma attuale (non sottoposta a transpilazione), il nostro circuito contiene 4 layer unici di gate a 2 qubit:
slices[1]: che ha gateRxxche agiscono su tutte le coppie dispari di qubitslices[2]: che ha gateRxxche agiscono su tutte le coppie pari di qubitslices[3]: che ha gateRyyche agiscono su tutte le coppie dispari di qubitslices[4]: che ha gateRyyche agiscono su tutte le coppie pari di qubit
Nella cella seguente costruiamo manualmente 4 istanze di LayerError, una per ciascuno di questi layer, con tassi di errore casuali.
import numpy as np
from qiskit.quantum_info import PauliList
from qiskit_ibm_runtime.utils.noise_learner_result import (
LayerError,
PauliLindbladError,
)
# fmt: off
pauli_errors_even = ['IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIXX', 'IIIIIIIIXY', 'IIIIIIIIXZ', 'IIIIIIIIYI', 'IIIIIIIIYX', 'IIIIIIIIYY', 'IIIIIIIIYZ', 'IIIIIIIIZI', 'IIIIIIIIZX', 'IIIIIIIIZY', 'IIIIIIIIZZ', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII', 'XIIIIIIIII', 'XXIIIIIIII', 'XYIIIIIIII', 'XZIIIIIIII', 'YIIIIIIIII', 'YXIIIIIIII', 'YYIIIIIIII', 'YZIIIIIIII', 'ZIIIIIIIII', 'ZXIIIIIIII', 'ZYIIIIIIII', 'ZZIIIIIIII']
pauli_errors_odd = ['IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII']
# fmt: on
np.random.seed(42)
layer_error_odd_xx = LayerError(
circuit=slices[1],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)
layer_error_even_xx = LayerError(
circuit=slices[2],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)
layer_error_odd_yy = LayerError(
circuit=slices[3],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)
layer_error_even_yy = LayerError(
circuit=slices[4],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)
Se avessi usato il NoiseLearner per identificare i layer unici di gate a 2 qubit del tuo circuito e caratterizzarne il rumore, otterresti un oggetto NoiseLearnerResult.
Questo risultato conterrebbe un elenco di oggetti LayerError, proprio come quelli che abbiamo costruito manualmente sopra.
Per ogni layer unico a 2 qubit, il LayerError contiene:
- il
QuantumCircuitche rappresenta quel layer di gate a 2 qubit - gli indici dei qubit su cui agisce questo circuito
- il
PauliLindbladErrorche rappresenta il modello di rumore caratterizzato di questo layer
Il PauliLindbladError conterrà due oggetti:
- l'elenco degli errori di Pauli che sono stati caratterizzati
- i tassi di errore corrispondenti a ciascuno di questi errori di Pauli
Normalmente, l'elenco degli errori di Pauli sarà sparso. Più precisamente, conterrà gli errori di Pauli a singolo qubit su tutti i qubit su cui agiscono delle gate, nonché gli errori di Pauli a due qubit su tutti i qubit che sono connessi.
Inserire i layer rumorosi nel nostro circuito
Nella sezione precedente abbiamo costruito specificamente un LayerError per ciascuno dei layer unici di gate a 2 qubit noti.
Questo significa che sappiamo esattamente quale LayerError corrisponde a una specifica delle nostre slice.
Normalmente, quando usi il LayerError, dovrai capire qual è il layer unico di gate a 2 qubit e dove compare all'interno del tuo circuito.
Dovrai poi adattare il tuo circuit e/o le tue slices per inserire di conseguenza il LayerError.
Come farlo nel caso generale va oltre lo scopo di questa guida pratica.
TODO: link to external documentation, once it exists!
Qui il nostro compito è più semplice perché sappiamo a quale slice corrisponde un LayerError.
Quindi si tratta solo di inserire nuove slice che rappresentino il rumore.
Nota che dobbiamo racchiudere ogni PauliLindbladError di LayerError.error in un PauliLindbladErrorInstruction affinché sia un'istruzione valida di QuantumCircuit.
from qiskit_addon_obp.utils.noise import PauliLindbladErrorInstruction
noisy_slices = []
for slice_ in slices:
if slice_ == layer_error_even_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_even_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_yy.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_yy.error
),
slice_.qubits,
)
]
)
)
noisy_slices.append(slice_)
Possiamo controllare il nostro lavoro e disegnare il circuito qui sotto:
combine_slices(noisy_slices, include_barriers=True).draw(
"mpl", fold=100, scale=0.8
)
Simulare un valore di aspettazione rumoroso
A questo punto, simulare classicamente il valore di aspettazione funziona esattamente come prima, solo
vacuum_state_noisy_obs, _, metadata = backpropagate(obs, noisy_slices)
vacuum_state_noisy_obs.coeffs[
~vacuum_state_noisy_obs.paulis.x.any(axis=1)
].sum()
np.complex128(-0.7230801696448901+7.082755280463563e-19j)
Sottolineiamo ancora che vanno considerati diversi aspetti legati alle prestazioni. Torna alla sezione corrispondente più sopra.
