Klasyczna symulacja obwodów za pomocą OBP
W tym przewodniku dowiesz się, jak klasycznie symulować instancje QuantumCircuit, aby szacować wartości oczekiwane wyłącznie za pomocą OBP.
Ponieważ OBP przyjmuje obserwablę i propaguje ją wstecznie przez dany obwód, „symulacja” obwodu sprowadza się do obliczenia wartości oczekiwanej docelowej obserwabli względem tego obwodu.
Jak zobaczysz później, pakiet qiskit-addon-obp potrafi nawet obsługiwać proste modele szumu, co pozwala obliczać także zaszumione wartości oczekiwane!
Budowa przykładowego obwodu
Na potrzeby tego przewodnika użyjemy tego samego przykładowego obwodu co w przewodniku o obcinaniu wyrazów Pauliego:
# 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)
Powyższy obwód opisuje jednak wyłącznie ewolucję w czasie pod wpływem wybranego hamiltonianu. Potrzebujemy też stanu początkowego, od którego zaczynamy i względem którego obliczamy wartości oczekiwane naszej obserwabli.
Oczywiście moglibyśmy wybrać jako stan początkowy stan samych zer (czyli próżni), ale aby pokazać, jak wstawić własny stan początkowy, poniżej wybieramy inny.
Jedną z możliwości byłoby dołączenie stanu początkowego na początku powyższego obwodu ewolucji w czasie: circuit.compose(initial_state, front=True).
Jednak ponieważ nasz circuit jest już podzielony na wycinki, łatwiej po prostu wstawić stan początkowy jako pierwszy wycinek, co robimy poniżej.
W ten sposób możemy po prostu zastąpić pierwszy wycinek innym stanem początkowym, jeśli zechcemy go w przyszłości zmienić, bez konieczności ponownego obliczania wycinków.
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)

Symulacja bezszumowej wartości oczekiwanej
Jako docelową obserwablę wybieramy obserwablę ZZ na centralnych kubitach:
from qiskit.quantum_info import SparsePauliOp
obs = SparsePauliOp("IIIIZZIIII")
Na tym etapie jesteśmy gotowi, aby klasycznie zasymulować wartość oczekiwaną za pomocą OBP.
W tym celu wystarczy przekazać metodzie backpropagate wszystkie wycinki, w ten sposób:
from qiskit_addon_obp import backpropagate
vacuum_state_obs, _, metadata = backpropagate(obs, slices)
Wykonaliśmy teraz propagację wsteczną naszej docelowej obserwabli obs przez cały obwód (wraz z initial_state, który umieściliśmy w slices[0]), w wyniku czego powstał nowy SparsePauliOp, którego wartość oczekiwaną otrzymujemy, rzutując go na stan próżni (|00...00>).
Można to zrobić w prosty sposób, sumując współczynniki wszystkich wyrazów Pauliego zdefiniowanych w bazie obliczeniowej:
vacuum_state_obs.coeffs[~vacuum_state_obs.paulis.x.any(axis=1)].sum()
np.complex128(-0.8285688012239535+4.9487770271457865e-20j)
Dla sprawdzenia poprawności (i aby dowieść, że to działa) możemy porównać nasz wynik z wynikiem Statevector z Qiskit:
from qiskit.quantum_info import Statevector
Statevector(combine_slices(slices)).expectation_value(obs)
np.complex128(-0.8285687255430366+0j)
Kilka uwag o wydajności
Wydajność obliczeniowa powyższego wywołania backpropagate będzie w dużym stopniu zależeć od wielu czynników, w tym od:
- struktury
circuit - metody dzielenia obwodu na wycinki
- docelowej obserwabli
- parametrów obcinania
Ponieważ metoda backpropagate upraszcza obserwablę po zastosowaniu każdego wycinka, liczba bramek w wycinku może dramatycznie wpływać na obciążenie obliczeniowe.
Najbardziej agresywną strategię upraszczania operatora można uzyskać, dzieląc obwód na wycinki zawierające pojedyncze bramki.
Dodatkowo możesz wykorzystać wszystkie mechanizmy obcinania wbudowane w metodę backpropagate.
Powyżej tego nie zrobiliśmy, co skutkuje dokładną wartością oczekiwaną, ale jak to zrobić, dowiesz się z przewodnika o obcinaniu wyrazów Pauliego.
Symulacja zaszumionej wartości oczekiwanej
Pakiet qiskit-addon-obp obsługuje także modele szumu w postaci PauliLindbladError.
Jest to szczególnie przydatne, gdy scharakteryzowałeś model szumu dwukubitowych warstw swojego obwodu, na przykład za pomocą NoiseLearner.
W tej sekcji zobaczysz, jak użyć obiektów LayerError zwracanych przez NoiseLearner, aby obliczać zaszumione wartości oczekiwane za pomocą OBP.
Uzyskiwanie modelu szumu
Zwykle uruchomiłbyś NoiseLearner, aby uzyskać model szumu swojego konkretnego obwodu.
Aby uniknąć złożoności (i losowości) w tym samouczku, zrezygnujemy z tego i zamiast tego poniżej na stałe zakodujemy pewien model szumu dla naszego obwodu.
Dbamy jednak o to, aby struktura naszych danych odpowiadała strukturze NoiseLearnerResult.
W obecnej (niztranspilowanej) postaci nasz obwód zawiera 4 unikalne warstwy bramek dwukubitowych:
slices[1]: zawiera bramkiRxxdziałające na wszystkich nieparzystych parach kubitówslices[2]: zawiera bramkiRxxdziałające na wszystkich parzystych parach kubitówslices[3]: zawiera bramkiRyydziałające na wszystkich nieparzystych parach kubitówslices[4]: zawiera bramkiRyydziałające na wszystkich parzystych parach kubitów
W poniższej komórce ręcznie konstruujemy 4 instancje LayerError dla każdej z tych warstw, z losowo dobranymi współczynnikami błędów.
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)),
),
)
Gdybyś użył NoiseLearner, aby zidentyfikować unikalne dwukubitowe warstwy bramek w swoim obwodzie i scharakteryzować ich szum, otrzymałbyś obiekt NoiseLearnerResult.
Ten wynik zawierałby listę obiektów LayerError, takich samych jak te, które skonstruowaliśmy ręcznie powyżej.
Dla każdej unikalnej warstwy dwukubitowej LayerError zawiera:
QuantumCircuitreprezentujący tę dwukubitową warstwę bramek- indeksy kubitów, na które ten obwód działa
PauliLindbladErrorreprezentujący scharakteryzowany model szumu tej warstwy
PauliLindbladError będzie zawierać dwa obiekty:
- listę scharakteryzowanych błędów Pauliego
- współczynniki błędów odpowiadające każdemu z tych błędów Pauliego
Zwykle lista błędów Pauliego będzie rzadka. Dokładniej, będzie zawierać jednokubitowe błędy Pauliego na wszystkich kubitach, na które działają bramki, oraz dwukubitowe błędy Pauliego na wszystkich tych kubitach, które są ze sobą połączone.
Wstawianie zaszumionych warstw do naszego obwodu
W poprzedniej sekcji skonstruowaliśmy specjalnie po jednym LayerError dla każdej ze znanych nam unikalnych dwukubitowych warstw bramek.
Oznacza to, że wiemy dokładnie, który LayerError pasuje do konkretnego z naszych wycinków.
Zwykle, używając LayerError, musisz ustalić, jaka jest unikalna dwukubitowa warstwa bramek i w którym miejscu obwodu występuje.
Następnie musisz dostosować swój circuit i/lub slices, aby odpowiednio wstawić LayerError.
Jak to zrobić w przypadku ogólnym, wykracza poza zakres tego przewodnika.
TODO: link to external documentation, once it exists!
Tutaj mamy prościej, ponieważ wiemy, któremu wycinkowi odpowiada dany LayerError.
Wystarczy więc wstawić nowe wycinki reprezentujące szum.
Zauważ, że każdy PauliLindbladError z LayerError.error musimy opakować w PauliLindbladErrorInstruction, aby był poprawną instrukcją 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_)
Możemy sprawdzić naszą pracę i narysować obwód poniżej:
combine_slices(noisy_slices, include_barriers=True).draw(
"mpl", fold=100, scale=0.8
)
Symulacja zaszumionej wartości oczekiwanej
Na tym etapie klasyczna symulacja wartości oczekiwanej działa dokładnie tak samo jak wcześniej, tylko
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)
Ponownie zwracamy uwagę, że należy wziąć pod uwagę liczne kwestie wydajnościowe. Wróć do odpowiedniej sekcji powyżej.
