Перейти до основного вмісту
Цей проєкт у бета-версії. Знайшов(-ла) помилку? Відкрити issue на GitHub. Є запитання чи ідея? Запитати в Discussions — будемо раді твоєму відгуку!
Яка якість перекладу на цій сторінці?

Класична симуляція схем за допомогою OBP

У цьому посібнику ти дізнаєшся, як класично симулювати екземпляри QuantumCircuit, щоб оцінювати очікувані значення повністю засобами OBP.

Оскільки OBP бере спостережувану й поширює її назад через задану схему, «симуляція» схеми зводиться до обчислення очікуваного значення цільової спостережуваної щодо цієї схеми. Як ти побачиш далі, пакет qiskit-addon-obp здатний навіть опрацьовувати прості моделі шуму, що дозволяє обчислювати й зашумлені очікувані значення!

Побудова прикладу схеми​

Для цілей цього посібника ми використаємо ту саму приклади схему, що й у посібнику з усічення доданків Паулі:

# 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)

Проте наведене вище — це лише схема, що описує часову еволюцію за обраним гамільтоніаном. Нам також потрібен початковий стан, від якого стартувати, щодо якого ми обчислюємо очікувані значення нашої спостережуваної.

Звісно, ми могли б обрати за початковий стан повністю нульовий (або вакуумний) стан, але щоб показати, як вставити власний початковий стан, нижче ми обираємо інший.

Одна з можливостей — додати початковий стан на початок нашої схеми часової еволюції вище: circuit.compose(initial_state, front=True). Але оскільки ми вже розрізали нашу circuit, простіше просто вставити початковий стан як перший зріз, що ми й робимо нижче.

Таким чином, ми можемо просто замінити перший зріз іншим початковим станом, якщо захочемо змінити його в майбутньому, без необхідності перераховувати наші зрізи.

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)

Output of the previous code cell

Симуляція безшумового очікуваного значення​

Як цільову спостережувану ми обираємо спостережувану ZZ на центральних кубітах:

from qiskit.quantum_info import SparsePauliOp

obs = SparsePauliOp("IIIIZZIIII")

На цьому етапі ми вже готові класично симулювати очікуване значення за допомогою OBP. Для цього ми просто передаємо всі зрізи методу backpropagate, ось так:

from qiskit_addon_obp import backpropagate

vacuum_state_obs, _, metadata = backpropagate(obs, slices)

Тепер ми зворотно поширили нашу цільову спостережувану obs через всю схему (включно з initial_state, який ми розмістили в slices[0]), отримавши новий SparsePauliOp, очікуване значення якого ми одержуємо проєктуванням на вакуумний стан (|00...00>).

Це можна зробити простим способом, підсумувавши коефіцієнти всіх доданків Паулі, визначених у обчислювальному базисі:

vacuum_state_obs.coeffs[~vacuum_state_obs.paulis.x.any(axis=1)].sum()
np.complex128(-0.8285688012239535+4.9487770271457865e-20j)

Для перевірки здорового глузду (і щоб довести, що це працює) ми можемо порівняти наш результат із Statevector з Qiskit:

from qiskit.quantum_info import Statevector

Statevector(combine_slices(slices)).expectation_value(obs)
np.complex128(-0.8285687255430366+0j)

Деякі зауваження щодо продуктивності​

Обчислювальна ефективність наведеного вище виклику backpropagate сильно залежатиме від багатьох чинників, зокрема:

  • структури circuit
  • методу розрізання схеми
  • цільової спостережуваної
  • параметрів усічення

Оскільки метод backpropagate спрощує спостережувану після застосування кожного зрізу, кількість вентилів у зрізі може суттєво впливати на обчислювальне навантаження. Найагресивнішу стратегію щодо спрощення оператора можна реалізувати, розрізавши схему на зрізи з окремих вентилів.

Крім того, ти можеш скористатися всіма механізмами усічення, вбудованими в метод backpropagate. Ми не робили цього вище, що фактично дало точне очікуване значення, але як це зробити, можна дізнатися з посібника з усічення доданків Паулі.

Симуляція зашумленого очікуваного значення​

Пакет qiskit-addon-obp також підтримує опрацювання моделей шуму у вигляді PauliLindbladError. Це особливо корисно, коли ти охарактеризував модель шуму 2-кубітних шарів у своїй схемі, наприклад за допомогою NoiseLearner.

У цьому розділі ти побачиш, як використовувати об'єкти LayerError, які повертає NoiseLearner, для обчислення зашумлених очікуваних значень за допомогою OBP.

Отримання моделі шуму​

Зазвичай ти виконав би NoiseLearner, щоб отримати модель шуму для твоєї конкретної схеми. Щоб уникнути складності (та випадковості) в цьому посібнику, ми не робитимемо цього, а натомість нижче жорстко задамо якусь модель шуму для нашої схеми.

Проте ми гарантуємо, що структура наших даних відповідає структурі NoiseLearnerResult. У поточному (нетранспільованому) вигляді наша схема містить 4 унікальні шари 2-кубітних вентилів:

  • slices[1]: містить вентилі Rxx, що діють на всі непарні пари кубітів
  • slices[2]: містить вентилі Rxx, що діють на всі парні пари кубітів
  • slices[3]: містить вентилі Ryy, що діють на всі непарні пари кубітів
  • slices[4]: містить вентилі Ryy, що діють на всі парні пари кубітів

У комірці нижче ми вручну створюємо 4 екземпляри LayerError для кожного з цих шарів із деякими випадковими показниками похибок.

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)),
),
)

Якби ти використав NoiseLearner для визначення унікальних 2-кубітних шарів вентилів твоєї схеми та характеризації їхнього шуму, ти отримав би об'єкт NoiseLearnerResult. Цей результат містив би список об'єктів LayerError, так само як ті, що ми створили вручну вище.

Для кожного унікального 2-кубітного шару LayerError містить:

  • QuantumCircuit, що представляє цей 2-кубітний шар вентилів
  • індекси кубітів, на які діє ця схема
  • PauliLindbladError, що представляє охарактеризовану модель шуму цього шару

PauliLindbladError міститиме два об'єкти:

  • список охарактеризованих похибок Паулі
  • показники похибок, що відповідають кожній із цих похибок Паулі

Зазвичай список похибок Паулі буде розрідженим. Точніше, він міститиме однокубітні похибки Паулі на всіх кубітах, на які діють вентилі, а також дво­кубітні похибки Паулі на всіх тих кубітах, що з'єднані між собою.

Вставлення зашумлених шарів у нашу схему​

У попередньому розділі ми спеціально створили по одному LayerError для кожного з наших відомих унікальних 2-кубітних шарів вентилів. Це означає, що ми знаємо, який LayerError точно відповідає конкретному з наших зрізів.

Зазвичай, використовуючи LayerError, потрібно з'ясувати, який унікальний 2-кубітний шар вентилів існує і де він зустрічається у твоїй схемі. Потім потрібно відкоригувати circuit та/або slices, щоб відповідно вставити LayerError. Як це робити в загальному випадку, виходить за межі цього практичного посібника. TODO: link to external documentation, once it exists! Тут усе простіше, бо ми знаємо, якому зрізу відповідає LayerError. Тому залишається лише вставити нові зрізи, що представляють шум.

Зауваж, що ми маємо обгорнути кожен PauliLindbladError з LayerError.error у PauliLindbladErrorInstruction, щоб він був коректною інструкцією 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_)

Ми можемо перевірити нашу роботу й намалювати схему нижче:

combine_slices(noisy_slices, include_barriers=True).draw(
"mpl", fold=100, scale=0.8
)

Output of the previous code cell

Симуляція зашумленого очікуваного значення​

На цьому етапі класична симуляція очікуваного значення працює точно так само, як і раніше, лише

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)

Ми ще раз наголошуємо, що слід враховувати кілька міркувань щодо продуктивності. Будь ласка, поверніться до відповідного розділу вище.