Klasická simulace obvodů pomocí OBP
V tomto průvodci se naučíš, jak klasicky simulovat instance QuantumCircuit a odhadovat střední hodnoty výhradně prostředky OBP.
Protože OBP vezme observablu a zpětně ji propaguje daným obvodem, „simulace“ obvodu spočívá ve výpočtu střední hodnoty cílové observably vůči tomuto obvodu.
Jak uvidíš později, balíček qiskit-addon-obp zvládne dokonce i jednoduché modely šumu, takže můžeš počítat i zašumělé střední hodnoty!
Sestavení ukázkového obvodu
Pro účely tohoto průvodce použijeme stejný ukázkový obvod jako v průvodci zkracováním Pauliho členů:
# 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)
Výše uvedené je však pouze obvod popisující časový vývoj pod zvoleným hamiltoniánem. Potřebujeme také počáteční stav, z něhož vyjdeme a vůči němuž vypočítáme střední hodnoty naší observably.
Samozřejmě bychom mohli jako počáteční stav zvolit stav samých nul (neboli vakuum), ale abychom ukázali, jak vložit vlastní počáteční stav, zvolíme níže jiný.
Jednou možností by bylo připojit počáteční stav na začátek našeho obvodu časového vývoje výše: circuit.compose(initial_state, front=True).
Protože jsme ale circuit už rozřezali, je jednodušší vložit počáteční stav prostě jako první řez, což děláme níže.
Tímto způsobem můžeme první řez jednoduše nahradit jiným počátečním stavem, budeme-li ho chtít v budoucnu změnit, aniž bychom museli znovu počítat řezy.
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)

Simulace střední hodnoty bez šumu
Jako cílovou observablu zvolíme observablu ZZ na prostředních qubitech:
from qiskit.quantum_info import SparsePauliOp
obs = SparsePauliOp("IIIIZZIIII")
V tuto chvíli jsme připraveni klasicky simulovat střední hodnotu pomocí OBP.
Stačí metodě backpropagate předat všechny řezy, takto:
from qiskit_addon_obp import backpropagate
vacuum_state_obs, _, metadata = backpropagate(obs, slices)
Nyní jsme naši cílovou observablu obs zpětně propagovali celým obvodem (včetně initial_state, který jsme umístili do slices[0]), což vedlo k novému SparsePauliOp, jehož střední hodnotu získáme jeho projekcí na vakuový stav (|00...00>).
Toho lze dosáhnout přímočaře sečtením koeficientů všech Pauliho členů definovaných ve výpočetní bázi:
vacuum_state_obs.coeffs[~vacuum_state_obs.paulis.x.any(axis=1)].sum()
np.complex128(-0.8285688012239535+4.9487770271457865e-20j)
Jako kontrolu správnosti (a důkaz, že to funguje) můžeme porovnat náš výsledek s Statevector z Qiskit:
from qiskit.quantum_info import Statevector
Statevector(combine_slices(slices)).expectation_value(obs)
np.complex128(-0.8285687255430366+0j)
Několik poznámek k výkonu
Výpočetní efektivita výše uvedeného volání backpropagate bude silně záviset na mnoha věcech, mimo jiné na:
- struktuře
circuit - způsobu rozřezání obvodu
- cílové observabli
- parametrech zkracování
Protože metoda backpropagate zjednodušuje observablu po aplikaci každého řezu, může počet hradel v řezu dramaticky ovlivnit výpočetní zátěž.
Nejagresivnější strategie z hlediska zjednodušení operátoru se dosáhne rozřezáním obvodu na řezy jednotlivých hradel.
Navíc můžeš využít všechny mechanismy zkracování zabudované v metodě backpropagate.
Výše jsme to neudělali, což vedlo k přesné střední hodnotě, ale jak na to, se můžeš naučit v průvodci zkracováním Pauliho členů.
Simulace zašuměné střední hodnoty
Balíček qiskit-addon-obp také podporuje zpracování modelů šumu ve formě PauliLindbladError.
To je užitečné zejména tehdy, když jsi charakterizoval(a) model šumu 2-qubitových vrstev ve svém obvodu, například pomocí NoiseLearner.
V této části uvidíš, jak použít objekty LayerError vrácené NoiseLearner k výpočtu zašuměných středních hodnot pomocí OBP.
Získání modelu šumu
Normálně bys spustil(a) NoiseLearner, abys získal(a) model šumu svého konkrétního obvodu.
Abychom se v tomto tutoriálu vyhnuli složitosti (a náhodnosti), neuděláme to a místo toho si pro náš obvod níže model šumu napevno zadáme.
Zajistíme však, aby struktura našich dat odpovídala struktuře NoiseLearnerResult.
Ve své současné (netranspilované) podobě obsahuje náš obvod 4 jedinečné vrstvy 2-qubitových hradel:
slices[1]: která má hradlaRxxpůsobící na všechny liché dvojice qubitůslices[2]: která má hradlaRxxpůsobící na všechny sudé dvojice qubitůslices[3]: která má hradlaRyypůsobící na všechny liché dvojice qubitůslices[4]: která má hradlaRyypůsobící na všechny sudé dvojice qubitů
V buňce níže ručně sestavíme 4 instance LayerError pro každou z těchto vrstev s několika náhodnými mírami chyb.
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)),
),
)
Pokud bys použil(a) NoiseLearner k identifikaci jedinečných vrstev 2-qubitových hradel ve svém obvodu a k charakterizaci jejich šumu, získal(a) bys objekt NoiseLearnerResult.
Tento výsledek by obsahoval seznam objektů LayerError, stejně jako ty, které jsme výše sestavili ručně.
Pro každou jedinečnou 2-qubitovou vrstvu obsahuje LayerError:
QuantumCircuitpředstavující tuto vrstvu 2-qubitových hradel- indexy qubitů, na které tento obvod působí
PauliLindbladError, který představuje charakterizovaný model šumu této vrstvy
PauliLindbladError bude obsahovat dva objekty:
- seznam Pauliho chyb, které byly charakterizovány
- míry chyb odpovídající každé z těchto Pauliho chyb
Seznam Pauliho chyb bude normálně řídký. Přesněji bude obsahovat jednoqubitové Pauliho chyby na všech qubitech, na které působí hradla, a také dvouqubitové Pauliho chyby na všech těch qubitech, které jsou propojeny.
Vložení zašuměných vrstev do našeho obvodu
V předchozí části jsme pro každou z našich známých jedinečných vrstev 2-qubitových hradel záměrně sestavili jeden LayerError.
Znamená to, že víme, který LayerError přesně odpovídá kterému z našich řezů.
Při použití LayerError budeš normálně muset zjistit, jaká je jedinečná vrstva 2-qubitových hradel a kde se v tvém obvodu vyskytuje.
Pak budeš muset upravit svůj circuit a/nebo slices, aby se LayerError odpovídajícím způsobem vložil.
Jak na to v obecném případě, přesahuje rámec tohoto praktického průvodce.
TODO: odkaz na externí dokumentaci, až bude existovat!
Zde je naše situace jednodušší, protože víme, kterému řezu LayerError odpovídá.
Proto už jde jen o vložení nových řezů reprezentujících šum.
Všimni si, že každý PauliLindbladError z LayerError.error musíme zabalit do PauliLindbladErrorInstruction, aby se stal platnou instrukcí 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_)
Svou práci můžeme zkontrolovat a obvod vykreslit níže:
combine_slices(noisy_slices, include_barriers=True).draw(
"mpl", fold=100, scale=0.8
)
Simulace zašuměné střední hodnoty
V tuto chvíli funguje klasická simulace střední hodnoty úplně stejně jako dříve, jen
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)
Znovu upozorňujeme, že je třeba zvážit několik hledisek výkonu. Vrať se prosím k odpovídající části výše.
