Перейти до основного вмісту

Початок роботи з розрізанням схем за допомогою розрізів дротів

Версії пакетів

Код на цій сторінці розроблено з використанням наступних залежностей. Рекомендуємо використовувати ці версії або новіші.

qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-aer~=0.17
qiskit-addon-cutting~=0.10.0

Цей посібник демонструє практичний приклад розрізання дротів за допомогою пакету qiskit-addon-cutting. Він охоплює відновлення очікуваних значень семикубітної схеми з використанням розрізання дротів.

Розріз дроту представлений у цьому пакеті як двокубітна інструкція Move, яка визначається як скидання другого кубіта, на який діє інструкція, з подальшим обміном обох кубітів. Ця операція еквівалентна передачі стану першого кубіта другому кубіту з одночасним відкиданням вхідного стану другого кубіта.

Пакет розроблений так, щоб бути узгодженим із тим, як потрібно обробляти розрізи дротів при роботі з фізичними кубітами. Наприклад, розріз дроту може взяти стан фізичного кубіта nn і продовжити його як фізичний кубіт mm після розрізу. Можна думати про «розрізання інструкцій» як про єдину структуру для розгляду як розрізів дротів, так і розрізів вентилів у рамках одного формалізму (оскільки розріз дроту — це просто розрізана інструкція Move). Використання цієї структури для розрізання дротів також дозволяє повторне використання кубітів, що пояснюється в розділі про ручне розрізання дротів.

Однокубітна інструкція CutWire слугує більш абстрактним і простим інтерфейсом для роботи з розрізами дротів. Вона дозволяє позначити на високому рівні, де в схемі потрібно розрізати дріт, і доповнення для розрізання схем автоматично вставить відповідні інструкції Move.

Наступний приклад демонструє відновлення очікуваного значення після розрізання дротів. Ти створиш схему з кількома нелокальними вентилями та визначиш спостережувані для оцінки.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-aer qiskit-ibm-runtime
import numpy as np
from qiskit import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
from qiskit_ibm_runtime import SamplerV2, Batch
from qiskit_aer.primitives import EstimatorV2

from qiskit_addon_cutting.instructions import Move, CutWire
from qiskit_addon_cutting import (
partition_problem,
generate_cutting_experiments,
cut_wires,
expand_observables,
reconstruct_expectation_values,
)

qc_0 = QuantumCircuit(7)
for i in range(7):
qc_0.rx(np.pi / 4, i)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)
qc_0.cx(3, 4)
qc_0.cx(3, 5)
qc_0.cx(3, 6)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)

# Define observable
observable = SparsePauliOp(["ZIIIIII", "IIIZIII", "IIIIIIZ"])

# Draw circuit
qc_0.draw("mpl")

Output of the previous code cell

Розрізання дротів за допомогою високорівневої інструкції CutWire

Далі виконай розрізи дротів за допомогою однокубітної інструкції CutWire на кубіті q3q_3. Коли підексперименти будуть готові до виконання, використай функцію cut_wires(), щоб перетворити CutWire на інструкції Move на новостворених кубітах.

qc_1 = QuantumCircuit(7)
for i in range(7):
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(CutWire(), [3])
qc_1.cx(3, 4)
qc_1.cx(3, 5)
qc_1.cx(3, 6)
qc_1.append(CutWire(), [3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)

qc_1.draw("mpl")

Output of the previous code cell

Примітка про розширення спостережуваних

Коли схема розширюється через один або кілька розрізів дротів, спостережуване потрібно оновити, щоб врахувати додаткові кубіти, що вводяться. Пакет qiskit-addon-cutting має зручну функцію expand_observables(), яка приймає об'єкти PauliList та оригінальну й розширену схеми як аргументи і повертає новий PauliList.

Повернутий PauliList не міститиме жодної інформації про коефіцієнти оригінального спостережуваного, але їх можна ігнорувати аж до відновлення кінцевого очікуваного значення.

# Transform CutWire instructions to Move instructions
qc_2 = cut_wires(qc_1)

# Expand the observable to match the new circuit size
expanded_observable = expand_observables(observable.paulis, qc_0, qc_2)
print(f"Expanded Observable: {expanded_observable}")
qc_2.draw("mpl")
Expanded Observable: ['ZIIIIIIII', 'IIIZIIIII', 'IIIIIIIIZ']

Output of the previous code cell

Розбиття схеми та спостережуваного

Тепер задачу можна розділити на розбиття. Це виконується за допомогою функції partition_problem() з необов'язковим набором міток розбиття для визначення способу розділення схеми. Кубіти з однаковою міткою розбиття групуються разом, а будь-які нелокальні вентилі, що охоплюють більше одного розбиття, будуть розрізані.

Якщо мітки розбиття не надані, розбиття буде визначено автоматично на основі зв'язності схеми. Читай наступний розділ про ручне розрізання дротів для отримання додаткової інформації про включення міток розбиття.

partitioned_problem = partition_problem(
circuit=qc_2,
observables=expanded_observable,
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases

print(f"Subobservables to measure: \n{subobservables}\n")
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
subcircuits[0].draw("mpl")
Subobservables to measure:
{0: PauliList(['IIIII', 'ZIIII', 'IIIIZ']), 1: PauliList(['ZIII', 'IIII', 'IIII'])}

Sampling overhead: 256.0

Output of the previous code cell

subcircuits[1].draw("mpl")

Output of the previous code cell

У цій схемі розбиття ти розрізав два дроти, що призвело до накладних витрат вибірки 444^4.

Генерація підекспериментів для виконання та постобробки результатів

Для оцінки очікуваного значення повнорозмірної схеми з спільного квазі-імовірнісного розподілу розкладених вентилів генерується кілька підекспериментів, які потім виконуються на одному (або кількох) QPU. Метод generate_cutting_experiments робить це, приймаючи аргументи для словників subcircuits та subobservables, створених вище, а також кількість вибірок із розподілу.

Примітка про кількість вибірок

Аргумент num_samples визначає, скільки вибірок брати з квазі-імовірнісного розподілу, і визначає точність коефіцієнтів, що використовуються для відновлення. Передача нескінченності (np.inf) гарантує точний розрахунок усіх коефіцієнтів. Читай документацію API про генерацію ваг та генерацію експериментів розрізання для отримання додаткової інформації.

# Generate subexperiments
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits, observables=subobservables, num_samples=np.inf
)

# Set a backend to use and transpile the subexperiments
backend = FakeManilaV2()
pass_manager = generate_preset_pass_manager(
optimization_level=1, backend=backend
)
isa_subexperiments = {
label: pass_manager.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}

# Submit each partition's subexperiments to the Qiskit Runtime Sampler
# primitive, in a single batch so that the jobs will run back-to-back.
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}

Нарешті, очікуване значення повної схеми можна відновити за допомогою методу reconstruct_expectation_values().

Наведений нижче блок коду відновлює результати та порівнює їх із точним очікуваним значенням.

reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
# Apply the coefficients of the original observable
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)

# Compute the exact expectation value using the `qiskit_aer` package.
estimator = EstimatorV2()
exact_expval = estimator.run([(qc_0, observable)]).result()[0].data.evs
print(
f"Reconstructed expectation value: {np.real(np.round(reconstructed_expval, 8))}"
)
print(f"Exact expectation value: {np.round(exact_expval, 8)}")
print(
f"Error in estimation: {np.real(np.round(reconstructed_expval-exact_expval, 8))}"
)
print(
f"Relative error in estimation: {np.real(np.round((reconstructed_expval-exact_expval) / exact_expval, 8))}"
)
Reconstructed expectation value: 1.45965266
Exact expectation value: 1.59099026
Error in estimation: -0.1313376
Relative error in estimation: -0.08255085
Примітка про коефіцієнти спостережуваного

Для точного відновлення очікуваного значення до виводу відновлення необхідно застосувати коефіцієнти оригінального спостережуваного (які відрізняються від виводу generate_cutting_experiments()), оскільки ця інформація була втрачена під час генерації експериментів розрізання або розширення спостережуваного.

Зазвичай ці коефіцієнти можна застосувати через numpy.dot(), як показано раніше.

Розрізання дротів за допомогою низькорівневої інструкції Move

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

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

qc_1 = QuantumCircuit(8)
for i in [*range(4), *range(5, 8)]:
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(Move(), [3, 4])
qc_1.cx(4, 5)
qc_1.cx(4, 6)
qc_1.cx(4, 7)
qc_1.append(Move(), [4, 3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)

# Expand observable
observable_expanded = SparsePauliOp(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"])
qc_1.draw("mpl")

Output of the previous code cell

Схему вище тепер можна розбити та згенерувати експерименти розрізання. Щоб явно вказати, як потрібно розбити схему, можна додати мітки розбиття до функції partition_problem(). Кубіти з однаковою міткою розбиття групуються разом, а будь-які нелокальні вентилі, що охоплюють більше одного розбиття, будуть розрізані. Ключі словника, що повертається функцією partition_problem(), будуть відповідати тим, що вказані в рядку міток.

partitioned_problem = partition_problem(
circuit=qc_1,
partition_labels="AAAABBBB",
observables=observable_expanded.paulis,
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases

print(f"Subobservables to measure: \n{subobservables}\n")
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
subcircuits["A"].draw("mpl")
Subobservables to measure:
{'A': PauliList(['IIII', 'ZIII', 'IIIZ']), 'B': PauliList(['ZIII', 'IIII', 'IIII'])}

Sampling overhead: 256.0

Output of the previous code cell

subcircuits["B"].draw("mpl")

Output of the previous code cell

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

Наступні кроки

Рекомендації