Класичний зворотний зв'язок і керування потоком (динамічні схеми)
Версії пакетів
Код на цій сторінці розроблено з використанням наведених нижче залежностей. Рекомендуємо використовувати ці або новіші версії.
qiskit[all]~=2.4.0
Динамічні схеми — потужний інструмент, який дозволяє вимірювати кубіти посередині виконання квантової схеми і на основі результатів цих проміжних вимірювань виконувати класичні логічні операції всередині самої схеми. Цей процес також відомий як класичний зворотний зв'язок (classical feedforward). Хоча ми ще на початку шляху розуміння того, як найкраще використовувати динамічні схеми, квантова дослідницька спільнота вже визначила низку сценаріїв застосування, зокрема:
- Ефективна підготовка квантових станів, наприклад GHZ-стан, W-стан (докладніше про W-стан — у статті "State preparation by shallow circuits using feed forward"), а також широкий клас матричних добутків станів
- Ефективне заплутування на великі відстані між кубітами на одному чипі завдяки неглибоким схемам
- Ефективна вибірка з IQP-подібних схем
Qiskit підтримує чотири конструкції управління потоком для класичного зворотного зв'язку, кожна з яких реалізована як метод QuantumCircuit. Конструкції та їхні відповідні методи:
- Оператор if —
QuantumCircuit.if_test - Оператор switch —
QuantumCircuit.switch - Цикл for —
QuantumCircuit.for_loop - Цикл while —
QuantumCircuit.while_loop
Кожен з цих методів повертає контекстний менеджер і зазвичай використовується в операторі with. У решті цього посібника пояснюється кожна з цих конструкцій і способи їх використання.
Існують певні обмеження операцій класичного зворотного зв'язку та управління потоком на квантовому обладнанні, які можуть вплинути на твою програму. Докладніше — у розділі Виконання динамічних схем.
Оператор if
Оператор if використовується для умовного виконання операцій залежно від значення класичного біта або регістра.
У наведеному нижче прикладі застосовується вентиль Адамара до кубіта і виконується його вимірювання. Якщо результат дорівнює 1, то до кубіта застосовується вентиль X, який перевертає його назад у стан 0. Потім кубіт вимірюється знову. Результат повторного вимірювання має дорівнювати 0 з імовірністю 100%.
# Added by doQumentation — required packages for this notebook
!pip install -q qiskit
from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister
qubits = QuantumRegister(1)
clbits = ClassicalRegister(1)
circuit = QuantumCircuit(qubits, clbits)
(q0,) = qubits
(c0,) = clbits
circuit.h(q0)
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}
Оператору 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}
Крім умови на один класичний біт, можна також задавати умову на значення класичного регістра, що складається з кількох бітів.
У наведеному нижче прикладі до двох кубітів застосовуються вентилі Адамара і виконується їх вимірювання. Якщо результат дорівнює 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}
Оператор switch
Оператор switch використовується для вибору дій залежно від значення класичного біта або регістра. Він схожий на оператор if, але дозволяє вказати більше варіантів для логіки розгалуження. У прикладі нижче до кубіта застосовується вентиль Адамара і виконується вимірювання. Якщо результат дорівнює 0, до кубіта застосовується вентиль X; якщо результат дорівнює 1, застосовується вентиль Z. Результат повторного вимірювання має дорівнювати 1 з імовірністю 100%.
qubits = QuantumRegister(1)
clbits = ClassicalRegister(1)
circuit = QuantumCircuit(qubits, clbits)
(q0,) = qubits
(c0,) = clbits
circuit.h(q0)
circuit.measure(q0, c0)
with circuit.switch(c0) as case:
with case(0):
circuit.x(q0)
with case(1):
circuit.z(q0)
circuit.measure(q0, c0)
circuit.draw("mpl")
# example output counts: {'1': 1024}
Оскільки у прикладі вище використовувався один класичний біт, було лише два можливі варіанти, тому того самого результату можна було досягти за допомогою оператора if-else. Оператор switch особливо корисний при розгалуженні на значення класичного регістра, що складається з кількох бітів. Наступний приклад показує, як побудувати варіант за замовчуванням, який виконується, якщо жоден із попередніх варіантів не підходить. Зверни увагу, що в операторі switch виконується лише один блок. Провалу між варіантами немає.
У прикладі нижче до двох кубітів застосовуються вентилі Адамара і виконується їх вимірювання. Якщо результат дорівнює 00 або 11, до третього кубіта застосовується вентиль Z. Якщо результат дорівнює 01, застосовується вентиль Y. Якщо жоден із попередніх варіантів не збігся, застосовується вентиль X. Наприкінці вимірюється третій кубіт.
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.switch(clbits) as case:
with case(0b000, 0b011):
circuit.z(q2)
with case(0b001):
circuit.y(q2)
with case(case.DEFAULT):
circuit.x(q2)
circuit.measure(q2, c2)
circuit.draw("mpl")
# example output counts: {'101': 267, '110': 249, '011': 265, '000': 243}
Цикл for
Цикл for використовується для ітерації по послідовності класичних значень і виконання певних операцій на кожній ітерації.
У наступному прикладі цикл for застосовується для 5-разового застосування вентиля X до кубіта, після чого виконується його вимірювання. Оскільки виконується непарна кількість вентилів X, загальний ефект полягає у переведенні кубіта зі стану 0 у стан 1.
qubits = QuantumRegister(1)
clbits = ClassicalRegister(1)
circuit = QuantumCircuit(qubits, clbits)
(q0,) = qubits
(c0,) = clbits
with circuit.for_loop(range(5)) as _:
circuit.x(q0)
circuit.measure(q0, c0)
circuit.draw("mpl")
# example output counts: {'1': 1024}
Цикл while
Цикл while використовується для повторення інструкцій доти, доки виконується певна умова.
У прикладі нижче до двох кубітів застосовуються вентилі Адамара і виконується їх вимірювання. Потім створюється цикл while, що повторює цю процедуру доти, доки результат вимірювання дорівнює 11. В результаті фінальний результат вимірювання ніколи не має бути 11, а решта можливостей з'являтимуться приблизно з однаковою частотою.
qubits = QuantumRegister(2)
clbits = ClassicalRegister(2)
circuit = QuantumCircuit(qubits, clbits)
q0, q1 = qubits
c0, c1 = clbits
circuit.h([q0, q1])
circuit.measure(q0, c0)
circuit.measure(q1, c1)
with circuit.while_loop((clbits, 0b11)):
circuit.h([q0, q1])
circuit.measure(q0, c0)
circuit.measure(q1, c1)
circuit.draw("mpl")
# example output counts: {'01': 334, '10': 368, '00': 322}
Класичні вирази
Модуль класичних виразів Qiskit qiskit.circuit.classical містить експериментальне представлення операцій виконання над класичними значеннями під час виконання схеми.
Наступний приклад демонструє, що за допомогою обчислення парності можна створити n-кубітний GHZ-стан, використовуючи динамічні схеми. Спочатку генеруються пар Белла на сусідніх кубітах. Потім ці пари з'єднуються між собою шаром вентилів CNOT між парами. Далі вимірюється цільовий кубіт усіх попередніх вентилів CNOT, і кожен виміряний кубіт скидається до стану . До кожного невиміряного вузла, для якого парність усіх попередніх бітів непарна, застосовується . Нарешті, до виміряних кубітів застосовуються вентилі 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)
Store
Можна використовувати інструкцію store, щоб зберегти результат класичного виразу, якщо цей вираз буде використовуватися неодноразово. Операції автоматично розпаралелюються, що робить код значно ефективнішим під час виконання.
Наприклад, природніше і ефективніше під час виконання записати , де , ніж . Перший варіант обчислює заперечення за один паралельний крок перед ланцюжком XOR, замість того щоб обчислювати кожне заперечення послідовно всередині виразу.
Повний приклад:
from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.classical import expr
qregs = QuantumRegister(4, "q")
creg = ClassicalRegister(3, "c")
# temp is a plain ClassicalRegister used as the store target
temp = ClassicalRegister(3, "temp")
qc = QuantumCircuit(qregs, creg, temp)
qc.h([0, 1, 2])
qc.measure([0, 1, 2], creg)
# Store bit-NOT of the full 3-bit register into temp
qc.store(temp, expr.bit_not(creg))
# Compute parity of temp using bit-indexed XOR
parity = expr.bit_xor(
expr.bit_xor(expr.index(temp, 0), expr.index(temp, 1)),
expr.index(temp, 2),
)
# Flip q3 if parity of ~creg is 1
with qc.if_test(parity):
qc.x(3)
qc.measure([0, 1, 2], creg)
qc.draw("mpl")
Наступні кроки
- Дізнайся, як реалізувати точне динамічне роз'єднання за допомогою stretch.
- Використовуй візуалізацію розкладу схеми для відлагодження та оптимізації динамічних схем.
- Виконуй динамічні схеми.