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

Розрізання дроту у вигляді двокубітної інструкції `Move`

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

Ось кроки, які ми виконаємо в цьому шаблоні Qiskit:

  • Крок 1: Відображення задачі на квантові схеми та оператори:
    • Відобразити гамільтоніан на квантову схему.
  • Крок 2: Оптимізація для цільового апаратного забезпечення [Використовує аддон cutting]:
    • Розрізати схему та спостережуваний оператор.
    • Транспілювати підексперименти для апаратного забезпечення.
  • Крок 3: Виконання на цільовому апаратному забезпеченні:
    • Запустити підексперименти, отримані на кроці 2, використовуючи примітив Sampler.
  • Крок 4: Постобробка результатів [Використовує аддон cutting]:
    • Об'єднати результати кроку 3 для відновлення математичного сподівання відповідного спостережуваного оператора.

Крок 1: Відображення

Створення схеми для розрізання

Спочатку ми починаємо зі схеми, натхненної рис. 1(a) зі статті arXiv:2302.03366v1.

# 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

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)
<qiskit.circuit.instructionset.InstructionSet at 0x7f16ab191a80>
qc_0.draw("mpl")

Quantum circuit diagram

Задання спостережуваного оператора

from qiskit.quantum_info import SparsePauliOp

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

Крок 2: Оптимізація

Створення нової схеми, де інструкції Move розміщені в бажаних місцях розрізання

З огляду на наведену вище схему, ми хочемо зробити два розрізи дроту на лінії середнього кубіта, щоб схема могла розділитися на дві схеми по чотири кубіти кожна. Один зі способів зробити це — вручну розмістити двокубітні інструкції Move, які переносять стан з одного кубітного дроту на інший. Інструкція Move концептуально еквівалентна операції скидання на другому кубіті, за якою слідує вентиль SWAP. Ефект цієї інструкції полягає в перенесенні стану першого (джерельного) кубіта на другий (цільовий) кубіт із відкиданням вхідного стану другого кубіта. Для правильної роботи важливо, щоб другий (цільовий) кубіт не мав заплутаності з рештою системи; інакше операція скидання призведе до часткового колапсу стану решти системи.

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

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

from qiskit_addon_cutting.instructions import 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)

qc_1.draw("mpl")

Quantum circuit diagram

Створення спостережуваного оператора для нової схеми

Цей спостережуваний оператор відповідає observable, але ми маємо коректно врахувати додатковий кубітний дріт, що був доданий (тобто вставляємо "I" за індексом 4). Зауваж, що в Qiskit рядкове представлення кубіта-0 відповідає крайньому правому символу Паулі.

observable_expanded = SparsePauliOp(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"])

Розділення схеми та спостережуваних операторів

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

from qiskit_addon_cutting import 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

Візуалізація розкладеної задачі

subobservables
{'A': PauliList(['IIII', 'ZIII', 'IIIZ']),
'B': PauliList(['ZIII', 'IIII', 'IIII'])}
subcircuits["A"].draw("mpl")

Quantum circuit diagram

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

Quantum circuit diagram

Обчислення надлишку вибірки для обраних розрізів

Тут ми розрізаємо два дроти, що призводить до надлишку вибірки 444^4.

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

print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
Sampling overhead: 256.0

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

generate_cutting_experiments приймає аргументи circuits/observables як словники, що відображають мітки розбиття кубітів на відповідні subcircuit/subobservables.

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

from qiskit_addon_cutting import generate_cutting_experiments

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

Вибір бекенду

Тут ми використовуємо фейковий бекенд, що змусить Qiskit Runtime працювати в локальному режимі (тобто на локальному симуляторі).

from qiskit_ibm_runtime.fake_provider import FakeManilaV2

backend = FakeManilaV2()

Підготовка підекспериментів для бекенду

Перед надсиланням схем до Qiskit Runtime їх необхідно транспілювати з нашим бекендом як цільовим.

from qiskit.transpiler import generate_preset_pass_manager

# Transpile the subexperiments to ISA circuits
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()
}

Крок 3: Виконання

Запуск підекспериментів за допомогою примітиву Qiskit Runtime Sampler

from qiskit_ibm_runtime import SamplerV2, Batch

# 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()
}
/home/garrison/Qiskit/qiskit-ibm-runtime/qiskit_ibm_runtime/session.py:157: UserWarning: Session is not supported in local testing mode or when using a simulator.
warnings.warn(
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}

Крок 4: Постобробка

Відновлення математичного сподівання

Відновлюємо математичні сподівання для кожного доданку спостережуваного оператора та об'єднуємо їх для відновлення математичного сподівання вихідного спостережуваного оператора.

from qiskit_addon_cutting import reconstruct_expectation_values

reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)

Порівняння відновленого математичного сподівання з точним математичним сподіванням для вихідної схеми та спостережуваного оператора

from qiskit_aer.primitives import EstimatorV2

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.51319069
Exact expectation value: 1.59099026
Error in estimation: -0.07779957
Relative error in estimation: -0.04890009