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

Відображення представлень операторів

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

Реалізація власних мапперів​

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

Як конкретний приклад розглянемо перетворення Жордана-Вігнера, яке відображає MajoranaOperator на кубітний оператор за допомогою map_majorana_action_generators():

>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_fermions.mappers import map_majorana_action_generators
>>> from qiskit_fermions.operators import MajoranaOperator
>>>
>>> # Create a Majorana operator
>>> maj_op = MajoranaOperator.from_dict({(0, 1): 1.0, (2,): 0.5})
>>>
>>> # Define how each Majorana action maps to Pauli strings
>>> num_qubits = 2
>>> def map_action(mode: int) -> SparseObservable:
... idx = mode // 2
... qubits = list(range(idx + 1))
... pauli = "Y" if mode % 2 else "X"
... return SparseObservable.from_sparse_list(
... [("Z" * idx + pauli, qubits, 1.0)],
... num_qubits=num_qubits,
... )
>>>
>>> # Apply the mapping to transform the operator
>>> custom_observable = map_majorana_action_generators(
... maj_op,
... map_action,
... identity=lambda: SparseObservable.identity(num_qubits),
... compose=SparseObservable.compose,
... )
>>> print(custom_observable.simplify())
<SparseObservable with 2 terms on 2 qubits: (0+1j)(Z_0) + (0.5+0j)(X_1 Z_0)>

Ключові кроки для реалізації будь-якого власного мапера:

  1. Визнач перетворення дій: Вкажи, як кожна дія (індексована за модою) відображається на цільове подання. У наведеному вище прикладі дії Майорани відображаються на рядки Паулі за правилами Жордана-Вігнера.

  2. Використай одну з наданих функцій відображення: Модуль mappers надає допоміжні функції, які обробляють ітерацію за доданками оператора та подальше застосування власної карти дій для поданb операторів із модуля operators. Наприклад, map_majorana_action_generators() працює з операторами Майорани.

  3. Отримай результат: Функція відображення повертає оператор у цільовому поданні, у якому перетворення послідовно застосоване до всіх доданків.

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

Підказка

Ти також можеш перебирати доданки оператора вручну, замість того щоб використовувати надані ітераторні функції. Подробиці дивись у розділі про ітерацію за доданками у посібнику з операторів.

Реалізації з бібліотеки​

Модуль qiskit_fermions.mappers.library надає ефективні, ретельно протестовані реалізації поширених відображень. Вони дотримуються того самого базового шаблону, що й власні мапери, але оптимізовані для використання у продакшені та доступні як у Python, так і в C API.

У прикладі нижче бібліотечний мапер застосовується до того самого MajoranaOperator із розділу про власні мапери, що демонструє еквівалентні результати:

>>> from qiskit_fermions.mappers.library import majorana_to_fermion, jordan_wigner
>>>
>>> # Convert MajoranaOperator to FermionOperator using library mapper
>>> ferm_op = majorana_to_fermion(maj_op)
>>>
>>> # Apply Jordan-Wigner mapper to get qubit operator
>>> sparse_observable = jordan_wigner(ferm_op, num_qubits=2)
>>> difference = sparse_observable - custom_observable
>>> print(difference.simplify() == SparseObservable.zero(num_qubits))
True
Примітка

Двокроковий варіант вище розписано, щоб показати композицію двох бібліотечних мапперів. На практиці ти відображав би MajoranaOperator за один крок за допомогою majorana_jordan_wigner() (qf_maj_op_jordan_wigner() у C API), і так само для інших типів операторів – edge_vertex_jordan_wigner() та transfer_vertex_jordan_wigner() (qf_edge_op_jordan_wigner(), qf_transfer_op_jordan_wigner()). Проходження через FermionOperator також роздуває проміжний результат: кожен генератор цих алгебр відображається на єдиний рядок Паулі, тоді як кожна ферміонна дія відображається на суму з двох доданків, тож доданок, побудований із кількох генераторів, спочатку розростається, а потім знову зводиться. Ця економія зростає з довжиною доданків; для доданків з одним генератором у гамільтоніані стрибків обидва маршрути коштують приблизно однаково.

У Python API деякі з цих бібліотечних мапперів також доступні через типонезалежні зручні мапери. fermion_operator() та majorana_operator() стоять перед majorana_to_fermion() та fermion_to_majorana() (серед інших) і внутрішньо делегують роботу відповідній бібліотечній реалізації залежно від типу переданого об'єкта, тож тобі не потрібно самостійно обирати правильну функцію. Те саме стосується jordan_wigner() та ternary_tree() (про нього далі), кожна з яких передає виклик тій прямій реалізації, що відповідає переданому оператору. Про те, як цей диспетчинг реалізовано всередині, дивись qiskit_fermions.protocols.

Обери кодування за допомогою тернарних дерев​

Жордан-Вігнер – не єдине кодування ферміонів у кубіти, і не найдешевше. Його рядки ZZ зростають разом з індексом моди, тож оператор Майорани на останній з nn мод відображається на рядок Паулі ваги nn. Кодування, що розподіляють інформацію про парність по дереву, а не по ланцюжку, асимптотично кращі.

Тернарне дерево з nn вузлів охоплює всю цю родину. Кожен вузол – це кубіт із трьома низхідними зв'язками, позначеними X, Y і Z; зв'язок або веде до дочірнього вузла (ребро), або закінчується (листок). Шлях кожного листка назад до кореня записує рядок Паулі: перетин зв'язку з позначкою PP, що веде від вузла uu, дає PP на кубіті uu. Кожне таке дерево дає коректне кодування, а відомі кодування – це просто певні форми: ланцюжок уздовж Z – це Жордан-Вігнер, а дерево, у якому кожен вузол має трьох дітей, наскільки дозволяє кількість вузлів, – збалансоване.

>>> from qiskit_fermions.mappers import TernaryTree, TernaryTreeEncoding
>>>
>>> # Jordan-Wigner is a chain descending along Z, with X and Y as legs at every node.
>>> TernaryTree.chain(13, "Z").max_weight
13
>>> # The balanced tree spreads the same information over depth log_3(2n+1).
>>> TernaryTree.breadth_first(13, 3).max_weight
3

Ця максимальна вага оптимальна. Jiang et al. доводять, що оператори Майорани будь-якого відображення ферміонів у кубіти для nn мод мають середню вагу Паулі щонайменше log⁡3(2n)\log_3(2n), тож жодне відображення не може опустити найбільшу з них нижче ⌈log⁡3(2n+1)⌉\lceil \log_3(2n+1) \rceil. Обидва дерева використовують рівно nn кубітів, тож виграш – у вазі Паулі, а не в кількості кубітів.

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

>>> from qiskit_fermions.mappers.library import fermion_ternary_tree
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> hamiltonian = FermionOperator.from_dict(
... {((True, 0), (False, 3)): 1.0, ((True, 3), (False, 0)): 1.0}
... )
>>> encoding = TernaryTreeEncoding(TernaryTree.breadth_first(4, 3))
>>> fermion_ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>

Відображення через ланцюжок уздовж Z точно відтворює fermion_jordan_wigner() (qf_ferm_op_jordan_wigner() у C API), у чому й полягає узагальнення:

>>> from qiskit_fermions.mappers.library import fermion_jordan_wigner
>>> chain = TernaryTreeEncoding(TernaryTree.chain(4, "Z"))
>>> direct = fermion_jordan_wigner(hamiltonian, 4)
>>> (fermion_ternary_tree(hamiltonian, chain, 4) - direct).simplify().num_terms
0

Кожне подання оператора має мапер на тернарних деревах, що віддзеркалює набір Жордана-Вігнера; повний перелік дивись у qiskit_fermions.mappers.library, а також Mapper Library у C API. У Python API вони також доступні через ternary_tree() – типонезалежну обгортку, яка обирає відповідний мапер для переданого оператора, так само як і jordan_wigner():

>>> from qiskit_fermions.mappers.library import ternary_tree
>>> ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>
Примітка

Деякі тотожності, що виконуються в Жордані-Вігнері, є властивостями ланцюжка, а не тернарних дерев. Оператор числа nj=(1−Zj)/2n_j = (1 - Z_j)/2 лише на ланцюжку; на іншому дереві ZjZ_j стає рядком ZZ над набором кубітів. Так само вершинний оператор VjV_j є оператором Паулі ваги 1 ZjZ_j лише на ланцюжку, де ланцюжки ZZ двох рядків Майорани скорочуються.

Оскільки кожен оператор числа все ще відображається на єдиний рядок ZZ, стан Фока під будь-яким тернарним деревом залишається базисним станом обчислювального базису, але біт qq більше не відповідає заселеності qq. Натомість заселеність jj – це парність виміряних бітів на носії образу njn_j. Зчитуй ці носії з мапера, а не виводь їх із дерева, щоб декодування залишалося коректним для будь-якої форми чи перестановки мод:

>>> for mode in range(4):
... number = FermionOperator.from_dict({((True, mode), (False, mode)): 1.0})
... print(mode, ternary_tree(number, encoding, 4).simplify())
0 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2 Z_1 Z_0)>
1 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_1)>
2 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2)>
3 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_3)>

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

Коли Evolution знижується через такий мапер, обгорни мапер у group_wise() (дивись Синтез вентиля Evolution у ферміонному просторі). Доданок стрибка відображається на два комутувальні рядки Паулі, жоден з яких окремо не зберігає кількість частинок; лише їхній добуток в еволюції її зберігає. У Жордані-Вігнері обидва рядки діють на одні й ті самі кубіти, тож спрощений порядок доданків тримає їх поруч. На інших деревах їхні носії відрізняються (як у Y_3 Z_1 Y_0 та X_3 Z_2 X_0 раніше), і необроблений, і спрощений порядки перемежовують їх з образами інших доданків, і тоді формула добутку на боці кубітів призводить до витоку кількості частинок.

Побудуй довільне дерево​

Конструктори ланцюжка та обходу в ширину охоплюють однорідні родини, але загальний інтерфейс приймає одну пару (parent, label) на вузол, з None для кореня (C API натомість приймає індекс кореня). Це важливо, бо дерева, які варто шукати, зазвичай не є ні ланцюжком, ні однорідними – наприклад, алгоритм Bonsai вирощує кістякове дерево графа зв'язності пристрою, тож його галуження відповідає цій зв'язності.

>>> # Node 0 branches three ways, node 1 twice, and node 4 carries a single child.
>>> tree = TernaryTree([None, (0, "X"), (0, "Y"), (0, "Z"), (1, "X"), (1, "Z"), (4, "Y")])
>>> tree.num_nodes, tree.num_legs, tree.max_weight
(7, 15, 4)
>>> tree.children(0)
{'X': 1, 'Y': 2, 'Z': 3}

Зверни увагу на кількість листків: вона завжди дорівнює 2n+12n+1, незалежно від форми. Вузол із меншою кількістю дітей не породжує менше рядків Паулі – його невикористані зв'язки є листками, а листок записує рядок так само, як і нащадки ребра. З цих 2n+12n+1 рядків 2n2n утворюють пари в оператори Майорани nn мод, і рівно один залишається.

Повна парність​

Цей залишковий листок – це повна ферміонна парність ∏j(1−2nj)\prod_j (1 - 2 n_j). Це властивість кодування, а не відображення якогось оператора, тож це метод самого кодування:

>>> TernaryTreeEncoding(TernaryTree.chain(4, "Z")).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_2 Z_1 Z_0)>
>>> TernaryTreeEncoding(TernaryTree.breadth_first(4, 3)).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_0)>

Як показує збалансований випадок, це не в загальному випадку рядок з усіх ZZ: це листок, до якого веде шлях з усіх ZZ, і він діє як одиниця на кожному вузлі, що не лежить на цьому шляху.

Важливо

Повна парність тут – це спостережувана для вимірювання, а не обмеження, яке слід накладати. Обидва її власні значення фізичні, тож не проєктуй стан на сектор +1+1: тернарне кодування витрачає один кубіт на моду й відображається на весь 2n2^n-вимірний простір, не залишаючи нефізичного підпростору, який можна було б відкинути.

Приклад: дерево Бравого-Кітаєва​

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

Його дерево будується рекурсивним поділом вузлів, по одному на моду. У діапазоні вузлів [l,r][l, r] нехай hh – найбільший степінь двійки, що вміщується; вузол, розміщений для цього діапазону, – це l+h−1l + h - 1, останній у цьому блоці. Його зв'язок X веде до дерева, побудованого з блоку перед ним, а зв'язок Z – до дерева, побудованого із залишку після нього, залишаючи зв'язок Y листком:

>>> def bravyi_kitaev_tree(num_modes: int) -> TernaryTree:
... """Builds the ternary tree of the Bravyi-Kitaev encoding on ``num_modes`` modes."""
... spec: list[tuple[int, str] | None] = [None] * num_modes
... def build(left: int, right: int) -> int | None:
... if left > right:
... return None # an empty range: the parent's link stays a leg
... block = 1
... while 2 * block <= right - left + 1:
... block *= 2
... node = left + block - 1
... for child, label in (
... (build(left, node - 1), "X"), # the block before this node
... (build(node + 1, right), "Z"), # the remainder after it
... ):
... if child is not None:
... spec[child] = (node, label)
... return node
... build(0, num_modes - 1)
... return TernaryTree(spec)

За чотирьох мод увесь діапазон є єдиним блоком, тож першим розміщується вузол 3, перед ним лежить [0,2][0, 2], а після нього нічого; цей піддіапазон дає вузол 1 над окремими вузлами 0 і 2. Компіляція дерева підтверджує, що кодування є саме Бравого-Кітаєва, а не просто деревом тієї ж форми – його перший образ Майорани дорівнює X0X1X3X_0 X_1 X_3:

>>> tree = bravyi_kitaev_tree(4)
>>> tree.children(1)
{'X': 0, 'Z': 2}
>>> encoding = TernaryTreeEncoding(tree)
>>> encoding.majorana_image(0)
[(0, 'X'), (1, 'X'), (3, 'X')]

Його повна парність робить попередній розділ конкретним: залишковий листок – це той, до якого веде шлях з усіх ZZ, і тут цей шлях зупиняється в корені, тож спостережувана – це одно-кубітний Z3Z_3, а не рядок по всіх чотирьох кубітах:

>>> encoding.total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3)>

Для масштабування потрібен лише більший аргумент. Оскільки жоден вузол ніколи не витрачає свій зв'язок Y на ребро, кожен розгалужується щонайбільше на дві сторони, тож вага Паулі зростає логарифмічно, а не лінійно:

>>> [bravyi_kitaev_tree(n).max_weight for n in (4, 8, 16, 32, 64)]
[3, 4, 5, 6, 7]