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

Класичний зворотний зв'язок і керування потоком

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

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

qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
Динамічні схеми тепер доступні на всіх бекендах

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

Динамічні схеми — потужний інструмент, який дозволяє вимірювати кубіти посередині виконання квантової схеми і на основі результатів цих проміжних вимірювань виконувати класичні логічні операції всередині самої схеми. Цей процес також відомий як класичний зворотний зв'язок (classical feedforward). Хоча ми ще на початку шляху розуміння того, як найкраще використовувати динамічні схеми, квантова дослідницька спільнота вже визначила низку сценаріїв застосування, зокрема:

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

Специфікація OpenQASM 3 визначає низку структур керування потоком, але Qiskit Runtime наразі підтримує лише умовний оператор if. У Qiskit SDK йому відповідає метод if_test класу QuantumCircuit. Цей метод повертає контекстний менеджер і зазвичай використовується в операторі with. У цьому посібнику описано, як застосовувати цей умовний оператор.

примітка

Приклади коду в цьому посібнику використовують стандартну інструкцію вимірювання для проміжних вимірювань. Однак рекомендується натомість використовувати інструкцію MidCircuitMeasure, якщо бекенд її підтримує. Дивись документацію з проміжних вимірювань для отримання деталей.

Оператор if

Оператор if використовується для умовного виконання операцій залежно від значення класичного біта або регістра.

У наведеному нижче прикладі застосовується вентиль Адамара до кубіта і виконується його вимірювання. Якщо результат дорівнює 1, то до кубіта застосовується вентиль X, який перевертає його назад у стан 0. Потім кубіт вимірюється знову. Результат повторного вимірювання має дорівнювати 0 з імовірністю 100%.

# Added by doQumentation — required packages for this notebook
!pip install -q qiskit qiskit-ibm-runtime
from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister

qubits = QuantumRegister(1)
clbits = ClassicalRegister(1)
circuit = QuantumCircuit(qubits, clbits)
(q0,) = qubits
(c0,) = clbits

circuit.h(q0)
# Use MidCircuitMeasure() if it's supported by the backend.
# circuit.append(MidCircuitMeasure(), [q0], [c0])
circuit.measure(q0, c0)
with circuit.if_test((c0, 1)):
circuit.x(q0)
circuit.measure(q0, c0)
circuit.draw("mpl")

# example output counts: {'0': 1024}

Output of the previous code cell

Оператору with можна передати ціль присвоєння, яка сама є контекстним менеджером — її можна зберегти та згодом використати для створення блоку else, що виконується тоді, коли вміст блоку if не виконується.

У наведеному нижче прикладі ініціалізуються регістри з двома кубітами та двома класичними бітами. До першого кубіта застосовується вентиль Адамара і виконується його вимірювання. Якщо результат дорівнює 1, до другого кубіта застосовується вентиль Адамара; інакше — вентиль X. Наприкінці вимірюється й другий кубіт.

qubits = QuantumRegister(2)
clbits = ClassicalRegister(2)
circuit = QuantumCircuit(qubits, clbits)
(q0, q1) = qubits
(c0, c1) = clbits

circuit.h(q0)
circuit.measure(q0, c0)
with circuit.if_test((c0, 1)) as else_:
circuit.h(q1)
with else_:
circuit.x(q1)
circuit.measure(q1, c1)

circuit.draw("mpl")

# example output counts: {'01': 260, '11': 272, '10': 492}

Output of the previous code cell

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

У наведеному нижче прикладі до двох кубітів застосовуються вентилі Адамара і виконується їх вимірювання. Якщо результат дорівнює 01 — тобто перший кубіт дорівнює 1, а другий — 0 — до третього кубіта застосовується вентиль X. Наприкінці вимірюється третій кубіт. Зауваж, що для наочності ми явно вказали стан третього класичного біта (0) в умові if. На схематичному зображенні схеми умова позначається кружечками на класичних бітах, на яких задається умова. Чорний кружечок означає умову на 1, а білий — умову на 0.

qubits = QuantumRegister(3)
clbits = ClassicalRegister(3)
circuit = QuantumCircuit(qubits, clbits)
(q0, q1, q2) = qubits
(c0, c1, c2) = clbits

circuit.h([q0, q1])
circuit.measure(q0, c0)
circuit.measure(q1, c1)
with circuit.if_test((clbits, 0b001)):
circuit.x(q2)
circuit.measure(q2, c2)

circuit.draw("mpl")

# example output counts: {'101': 269, '011': 260, '000': 252, '010': 243}

Output of the previous code cell

Класичні вирази

Модуль класичних виразів Qiskit qiskit.circuit.classical містить експериментальне представлення операцій виконання над класичними значеннями під час виконання схеми. Через апаратні обмеження наразі підтримуються лише умови QuantumCircuit.if_test().

Наступний приклад демонструє, що за допомогою обчислення парності можна створити n-кубітний GHZ-стан, використовуючи динамічні схеми. Спочатку генеруються n/2n/2 пар Белла на сусідніх кубітах. Потім ці пари з'єднуються між собою шаром вентилів CNOT між парами. Далі вимірюється цільовий кубіт усіх попередніх вентилів CNOT, і кожен виміряний кубіт скидається до стану 0\vert 0 \rangle. До кожного невиміряного вузла, для якого парність усіх попередніх бітів непарна, застосовується XX. Нарешті, до виміряних кубітів застосовуються вентилі CNOT для відновлення заплутаності, втраченої під час вимірювання.

При обчисленні парності перший елемент побудованого виразу передбачає підняття (lift) об'єкта Python mr[0] до вузла Value (lift використовується для перетворення довільних об'єктів на класичні вирази). Для mr[1] та можливих наступних класичних регістрів це не потрібно, оскільки вони є вхідними аргументами expr.bit_xor, а необхідне підняття виконується автоматично. Такі вирази можна будувати в циклах та інших конструкціях.

from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.classical import expr

num_qubits = 8
if num_qubits % 2 or num_qubits < 4:
raise ValueError("num_qubits must be an even integer ≥ 4")
meas_qubits = list(range(2, num_qubits, 2)) # qubits to measure and reset

qr = QuantumRegister(num_qubits, "qr")
mr = ClassicalRegister(len(meas_qubits), "m")
qc = QuantumCircuit(qr, mr)

# Create local Bell pairs
qc.reset(qr)
qc.h(qr[::2])
for ctrl in range(0, num_qubits, 2):
qc.cx(qr[ctrl], qr[ctrl + 1])

# Glue neighboring pairs
for ctrl in range(1, num_qubits - 1, 2):
qc.cx(qr[ctrl], qr[ctrl + 1])

# Measure boundary qubits between pairs,reset to 0
for k, q in enumerate(meas_qubits):
qc.measure(qr[q], mr[k])
qc.reset(qr[q])

# Parity-conditioned X corrections
# Each non-measured qubit gets flipped iff the parity (XOR) of all
# preceding measurement bits is 1
for tgt in range(num_qubits):
if tgt in meas_qubits: # skip measured qubits
continue
# all measurement registers whose physical qubit index < tgt
left_bits = [k for k, q in enumerate(meas_qubits) if q < tgt]
if not left_bits: # skip if list empty
continue

# build XOR-parity expression
parity = expr.lift(
mr[left_bits[0]]
) # lift the first bit to Value so it will be treated like a boolean.
for k in left_bits[1:]:
parity = expr.bit_xor(
mr[k], parity
) # calculate parity with all other bits
with qc.if_test(parity): # Add X if parity is 1
qc.x(qr[tgt])

# Re-entangle measured qubits
for ctrl in range(1, num_qubits - 1, 2):
qc.cx(qr[ctrl], qr[ctrl + 1])
qc.draw(output="mpl", style="iqp", idle_wires=False, fold=-1)

Output of the previous code cell

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

Щоб знайти всі бекенди, доступні твоєму акаунту і такі, що підтримують динамічні схеми, виконай код на зразок наведеного нижче. У цьому прикладі передбачається, що ти зберіг свої облікові дані. Також можна явно вказати облікові дані під час ініціалізації облікового запису служби Qiskit Runtime. Це дозволяє, наприклад, переглянути бекенди, доступні для конкретного екземпляра або типу тарифного плану.

Примітки
  • Бекенди, доступні акаунту, залежать від екземпляра, вказаного в облікових даних.
  • Нова версія динамічних схем тепер доступна всім користувачам на всіх бекендах. Дивись оголошення для отримання додаткових деталей.
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService()
dc_backends = service.backends(dynamic_circuits=True)
print(dc_backends)
[<IBMBackend('ibm_pittsburgh')>, <IBMBackend('ibm_boston')>, <IBMBackend('ibm_fez')>, <IBMBackend('ibm_miami')>, <IBMBackend('ibm_marrakesh')>, <IBMBackend('ibm_torino')>, <IBMBackend('ibm_kingston')>]

Обмеження Qiskit Runtime

Враховуй наступні обмеження під час запуску динамічних схем у Qiskit Runtime.

  • Через обмежений обсяг фізичної пам'яті на контролюючій електроніці існує також обмеження на кількість операторів if та розмір їхніх операндів. Це обмеження є функцією від кількості трансляцій (broadcasts) та кількості трансльованих бітів у задачі (не в схемі).

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

    c0 = ClassicalRegister(3)
    c1 = ClassicalRegister(5)
    ...
    with circuit.if_test((c0, 1)) ...
    with circuit.if_test((c0, 3)) ...
    with circuit.if_test((c1[2], 1)) ...

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

    Наразі, якщо щоразу транслювати по 60 бітів, задача може мати приблизно 300 трансляцій. Якщо ж щоразу транслювати лише один біт, задача може мати до 2400 трансляцій.

  • Операнд, що використовується в операторі if_test, має містити не більше 32 бітів. Тому, якщо ти порівнюєш цілий ClassicalRegister, його розмір має бути не більше 32 бітів. Однак якщо ти порівнюєш лише один біт із ClassicalRegister, то сам ClassicalRegister може мати будь-який розмір (оскільки операнд — лише один біт).

    Наприклад, блок коду «Не допустимо» не працює, оскільки cr містить більше 32 бітів. Водночас можна використовувати класичний регістр ширший за 32 біти, якщо перевіряти лише один біт, як показано в блоці «Допустимо».

       cr = ClassicalRegister(50)
    qr = QuantumRegister(50)
    circuit = QuantumCircuit(qr, cr)
    ...
    circ.measure(qr, cr)
    with circ.if_test((cr, 15)):
    ...
  • Вкладені умовні оператори не підтримуються. Наприклад, наведений нижче блок коду не працюватиме, оскільки містить if_test всередині іншого if_test:

       c1 = ClassicalRegister(1, "c1")
    c2 = ClassicalRegister(2, "c2")
    ...
    with circ.if_test((c1, 1)):
    with circ.if_test(c2, 1)):
    ...
  • Використання reset або вимірювань всередині умовних операторів не підтримується.

  • Арифметичні операції не підтримуються.

  • Дивись таблицю функцій OpenQASM 3, щоб дізнатися, які функції OpenQASM 3 підтримуються в Qiskit і Qiskit Runtime.

  • Коли OpenQASM 3 (замість QuantumCircuit) використовується як вхідний формат для передачі схем примітивам Qiskit Runtime, підтримуються лише інструкції, які можна завантажити до Qiskit. Класичні операції, наприклад, не підтримуються, оскільки їх не можна завантажити до Qiskit. Дивись Імпорт програми OpenQASM 3 до Qiskit для отримання додаткової інформації.

  • Інструкції for, while і switch не підтримуються.

Використання динамічних схем із Estimator

Оскільки Estimator не підтримує динамічні схеми, можна використовувати Sampler і будувати власні схеми вимірювань. Як альтернативу можна використовувати примітив Executor, який підтримує динамічні схеми.

Щоб відтворити поведінку Estimator, дотримуйся такого процесу:

  1. Згрупуй члени всіх спостережуваних у розбиття. Це можна зробити, наприклад, за допомогою API PauliList,
    примітка

    Можна використовувати атрибут примітива BitArray для обчислення математичних сподівань наданих спостережуваних.

  2. Виконай по одній схемі зміни базису для кожного розбиття (яку саме зміну базису потрібно виконати для кожного розбиття). Дивись утиліту надбудов для базисів вимірювань — модуль measurement_bases для отримання додаткової інформації. Починай роботу з утилітами.
  3. Підсумуй результати для кожного розбиття.

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

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