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

Принципи проєктування представлень операторів

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

BosonOperator — це точна сума добутків бозонних операторів народження й знищення з комплексними коефіцієнтами. Він не містить обрізання заповнення. Бозонна мода вміщує довільну кількість бозонів, тож її простір Фока нескінченновимірний, а усічення з'являється лише тоді, коли оператор відображається на кубіти (див. Усічення простору Фока). Тому все в цьому посібнику відбувається в точній алгебрі канонічних комутаційних співвідношень,

[bi,bj†]=δij,[bi,bj]=[bi†,bj†]=0 .[b_i, b_j^\dagger] = \delta_{ij}, \qquad [b_i, b_j] = [b_i^\dagger, b_j^\dagger] = 0 \, .

Два наслідки формують інтерфейс. Індекс моди може повторюватися в одному члені, бо bi†bi†b_i^\dagger b_i^\dagger є коректним оператором, що створює два бозони. Також перестановка двох множників ніколи не вводить знак, хоча перенесення оператора знищення повз оператор народження на тій самій моді додає член, bibi†=bi†bi+1b_i b_i^\dagger = b_i^\dagger b_i + 1.

Зберігання​

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

coeffsОдин комплексний коефіцієнт на кожен член.
actionsОдне булеве значення на множник: True для оператора народження і False для оператора знищення.
modesОдин індекс моди на множник.
boundariesПозиції в actions і modes, де кожен член починається і закінчується.

Конструктор безпосередньо приймає ці чотири масиви. Наведений нижче оператор має три члени, а саме 2b0†b12 b_0^\dagger b_1, −ib1†b1†-i b_1^\dagger b_1^\dagger і сталу 0.50.5, а масиви можна прочитати назад методами get_*:

>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>> op = BosonOperator(
... coeffs=[2.0, -1.0j, 0.5],
... actions=[True, False, True, True],
... modes=[0, 1, 1, 1],
... boundaries=[0, 2, 4, 4],
... )
>>> op.get_coeffs()
[(2+0j), (-0-1j), (0.5+0j)]
>>> op.get_actions()
[True, False, True, True]
>>> op.get_modes()
[0, 1, 1, 1]
>>> op.get_boundaries()
[0, 2, 4, 4]
>>> len(op)
3

Перший член охоплює позиції від 0 до 2 в actions і modes, другий — від 2 до 4, а третій — від 4 до 4, і саме так зберігається сталий член, тобто член без множників. Повторювана мода 1 у другому члені зберігається як записано; член ніколи не відкидається й не зливається через те, що індекс моди з'являється двічі.

Note

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

Така структура робить побудову й арифметику дешевими, з одним наслідком: оператор може містити той самий член кілька разів одночасно. Суми й добутки додають члени, але не зливають їх, тому викликай simplify(), коли потрібні унікальні члени (див. Арифметика).

Побудова операторів​

Найпряміший спосіб побудувати оператор — from_dict(), який приймає словник від кортежу множників до коефіцієнта. Функції cre() і ann() створюють множники, а порожній кортеж — це сталий член:

>>> b = BosonOperator.from_dict({(ann(0),): 1.0})
>>> b_dag = BosonOperator.from_dict({(cre(0),): 1.0})
>>> number = BosonOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> print(format(number))
1.000000e0 +0.000000e0j * (+0 -0)

У друкованому вигляді +0 позначає b0†b_0^\dagger, а -0 — b0b_0. Конструктори zero() і one() повертають адитивну та мультиплікативну одиницю, а from_terms() відновлює оператор з ітератора iter_terms().

Бібліотека операторів надає безрозмірні квадратури положення та імпульсу,

q^=b†+b2,p^=i(b†−b)2 ,\hat{q} = \frac{b^\dagger + b}{\sqrt{2}}, \qquad \hat{p} = \frac{i \left(b^\dagger - b\right)}{\sqrt{2}} \, ,

як position() і momentum(). Разом з функціями комутатора з того самого модуля ти можеш безпосередньо перевірити алгебру:

>>> from qiskit_bosons.operators.library import commutator, momentum, position
>>> q, p = position(0), momentum(0)
>>> q.is_hermitian() and p.is_hermitian()
True
>>> commutator(q, p).equiv(1j * BosonOperator.one())
True
>>> commutator(b, b_dag).equiv(BosonOperator.one())
True

Модуль також надає anti_commutator() і double_commutator(). Усі три приймають оператори одного типу й повертають новий оператор.

Стеж за порядком у добутку​

Два оператори можна перемножити двома способами, і їх легко сплутати. Оператор @ — це математичний добуток, тож a @ b є ABAB. Оператор & — це композиція, яка слідує конвенції Qiskit читання зліва направо: a & b спочатку застосовує a, а потім b, тож це BABA. Функції комутатора використовують @, тому commutator(a, b) є [A,B][A, B]. Якщо побудувати дужку самостійно через &, її знак непомітно інвертується:

>>> (b @ b_dag).equiv(b_dag @ b + BosonOperator.one())
True
>>> (b & b_dag).equiv(b_dag @ b)
True
>>> ((b @ b_dag) - (b_dag @ b)).equiv(BosonOperator.one())
True
>>> ((b & b_dag) - (b_dag & b)).equiv(-BosonOperator.one())
True

Огляд оператора​

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

>>> hopping = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(2), cre(2), ann(2), ann(2)): 0.5,
... }
... )
>>> len(hopping)
3
>>> sorted(hopping.iter_terms())
[([(True, 0), (False, 1)], (-1+0j)), ([(True, 1), (False, 0)], (-1+0j)), ([(True, 2), (True, 2), (False, 2), (False, 2)], (0.5+0j))]

Кілька методів підсумовують структуру оператора:

>>> hopping.get_support()
{0, 1, 2}
>>> hopping.min_num_modes()
3
>>> hopping.max_rank()
4

get_support() — це множина мод, на які діє оператор, min_num_modes() — найменший регістр, що може його вмістити, а max_rank() — довжина його найдовшого члена.

Note

Мода — це абстрактний індекс, що позначає один бозонний ступінь свободи. Вона не має власного змісту: ти визначаєш, який вузол ґратки, яку коливну координату чи який оптичний порт позначає мода. BosonicCircuit використовує ті самі індекси (див. Робота з бозонними схемами), тож оператор і вентилі, що його несуть, узгоджені щодо того, чим є мода 2, і жодному з них не потрібно знати, що вона представляє. Ніщо в операторі не фіксує, скільки рівнів має мода, і саме тому розмір регістра є лише нижньою межею; min_num_modes() — це найменший регістр, що вміщує оператор, а не його властивість.

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

>>> hopping.mode_occupations()
[1, 1, 2]
>>> hopping.mode_occupations([2, 0])
[2, 1]
>>> hopping.max_occupation()
2

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

>>> hopping.conserves_particle_number()
True
>>> move = BosonOperator.from_dict({(cre(0), ann(2)): 1.0})
>>> move.conserves_sector([3])
True
>>> move.conserves_sector([2, 1])
False

Нарешті, relabel_modes() перейменовує моди: мода i стає permutation[i]. Список має бути достатньо довгим, щоб індексувати кожну моду оператора, і не повинен повторювати елементи:

>>> print(format(move.relabel_modes([5, 6, 7])))
1.000000e0 +0.000000e0j * (+5 -7)

Арифметика​

Оператори підтримують додавання, віднімання, заперечення, а також множення й ділення на число, у звичайній операторній формі або на місці:

>>> total = 2 * b_dag + b_dag - 3j * b
>>> len(total)
3
>>> len(total.simplify())
2
>>> total.simplify().equiv(3 * b_dag - 3j * b)
True

Три члени total ніколи не зливалися, тож оператор містив b_dag двічі, доки simplify() не об'єднав їх. Метод підсумовує коефіцієнти однакових членів і відкидає ті, чия сумарна величина менша за допуск. Це структурна операція: вона порівнює записані множники й не застосовує комутаційних співвідношень. Споріднений метод ichop() видаляє малі коефіцієнти на місці без попереднього злиття, тому викликай simplify() перед ichop(), коли оператор може містити дублікати членів.

Добутки використовують @ або &, як описано вище, а ** підносить оператор до цілого степеня:

>>> x = b_dag + b
>>> print(format((x ** 2).simplify()))
1.000000e0 +0.000000e0j * (-0 -0)
1.000000e0 +0.000000e0j * (-0 +0)
1.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0)

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

Спряжений оператор змінює порядок множників у кожному члені на зворотний, міняє місцями оператори народження й знищення та спряжує коефіцієнти. Метод is_hermitian() перевіряє, чи дорівнює оператор своєму спряженому:

>>> hop = BosonOperator.from_dict({(cre(0), ann(1)): 1j})
>>> print(format(hop.adjoint()))
0.000000e0 -1.000000e0j * (+1 -0)
>>> hop.is_hermitian()
False
>>> (hop + hop.adjoint()).is_hermitian()
True

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

Нормальний порядок і порівняння операторів​

Два добутки драбинних операторів можуть бути одним і тим самим оператором і все одно виглядати по-різному, бо b b†b\,b^\dagger і b†b+1b^\dagger b + 1 рівні в алгебрі. Оператори, рівні в цьому сенсі, не обов'язково зберігаються однаково, тож для їх порівняння потрібні комутаційні співвідношення. Метод normal_ordered() застосовує їх: він переносить кожен оператор народження ліворуч від кожного оператора знищення й додає член щоразу, коли міняється місцями пара на тій самій моді.

>>> product = BosonOperator.from_dict({(ann(0), cre(0), ann(0), cre(0)): 1.0})
>>> ordered = product.normal_ordered()
>>> len(ordered)
5
>>> print(format(ordered.simplify()))
1.000000e0 +0.000000e0j * ()
3.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0 -0 -0)

Впорядкований оператор повертається як незлита сума з п'ятьма збереженими членами, які зводяться до трьох після того, як simplify() їх зливає. Нормальне впорядкування ніколи не змушує член зникнути: b†b†b^\dagger b^\dagger та кожен інший повторюваний множник залишаються.

Аргумент sandwich задає, як розташовані моди всередині двох груп множників. Типово обидві групи сортуються за спаданням. З sandwich=True оператори народження йдуть за зростанням, а оператори знищення — за спаданням, що розміщує більші індекси ближче до середини:

>>> pair = BosonOperator.from_dict({(cre(0), cre(1), ann(0), ann(1)): 1.0})
>>> list(pair.normal_ordered().iter_terms())
[([(True, 1), (True, 0), (False, 1), (False, 0)], (1+0j))]
>>> list(pair.normal_ordered(sandwich=True).iter_terms())
[([(True, 0), (True, 1), (False, 1), (False, 0)], (1+0j))]

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

>>> reordered = BosonOperator.from_dict({(cre(0), ann(0)): 1.0, (): 1.0})
>>> contracted = BosonOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> contracted == reordered
False
>>> contracted.simplify() == reordered.simplify()
False
>>> contracted.equiv(reordered)
True

Один лише метод simplify залишає дві форми розділеними, бо він зливає лише однакові записані члени. Так само is_hermitian() порівнює оператор із його спряженим у нормальному порядку, тому розпізнає b b†+b†bb\,b^\dagger + b^\dagger b як ермітів без додаткових зусиль.

Note

Нормальне впорядкування — точна тотожність в неусіченій алгебрі, і equiv на цьому ґрунтується. Після усічення це не тотожність. Відображений оператор залежить від форми, яку ти відображаєш, тому відображені образи contracted і reordered вище відрізняються, і лише на найвищому збереженому рівні. Про розмір різниці та вибір форми див. Усічення простору Фока.

Упорядкування й фільтрація членів​

Зміна порядку членів оператора, на відміну від множників усередині члена, не змінює оператор, але може змінити поведінку алгоритмів, які читають члени по порядку, як-от рандомізована формула добутку. Функції з qiskit_bosons.operators.terms повертають новий оператор і зберігають усі індекси груп, прикріплені до їхніх членів.

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

>>> from qiskit_bosons.operators.terms import canonical_order, order_terms
>>> mixed = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> list(canonical_order(mixed).iter_terms())
[([], (2+0j)), ([(True, 0), (False, 1)], (3+0j)), ([(True, 1), (False, 0)], (1+0j))]
>>> by_weight = order_terms(mixed, key=lambda term: abs(term[1]), reverse=True)
>>> [coeff for _, coeff in by_weight.iter_terms()]
[(3+0j), (2+0j), (1+0j)]

Якщо оператор має індекси груп (див. Групування членів оператора: використання структури оператора), функція group_order() робить кожну групу одним неперервним відрізком членів:

>>> from qiskit_bosons.operators.terms import group_order
>>> grouped = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> grouped.groups = [1, 0, 1]
>>> group_order(grouped).groups
[0, 1, 1]

Останній помічник видаляє члени, а не змінює їхній порядок. Член, що є добутком операторів числа, bi†bib_i^\dagger b_i, лише множить кожен стан Фока на фазу, тож filter_diagonal_terms() може відкинути сталу, оператори числа та їхні добутки, коли ця фаза не має значення для того, що ти обчислюєш. Він змінює оператор на місці й вимагає оператора в нормальному порядку, чого він не перевіряє:

>>> from qiskit_bosons.operators.terms import filter_diagonal_terms
>>> model = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(0), cre(0), ann(0), ann(0)): 0.5,
... (cre(0), ann(0)): 0.2,
... (): 0.1,
... }
... )
>>> filter_diagonal_terms(model)
>>> len(model)
2
>>> model.is_hermitian()
True

Залишаються лише два члени переходу. Взаємодія на вузлі й оператор числа діагональні в базисі заповнення, тому їх видалено разом зі сталою. Повторювана мода у взаємодії обробляється правильно: b0†b0†b0b0b_0^\dagger b_0^\dagger b_0 b_0 діагональний, тоді як b0†b0†b0b_0^\dagger b_0^\dagger b_0 не був би таким.

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