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

Оптимізація підготовки детермінанта Слейтера

Важливо

Концепції цього посібника наразі доступні лише в Python API. Еквівалентна функціональність стане доступною в C API у майбутньому релізі.

Вентиль InitializeModes задає опорну заселеність мод; вентиль OrbitalRotation обертає одночастинковий базис. Поставлені впритул, ці два вентилі готують детермінант Слейтера: моди починають в опорному детермінанті, а потім обертаються в цільові орбіталі. Вентиль PrepareSlaterDeterminant охоплює цю композицію, і оскільки він несе опорну заселеність, його транспіляція може використати прямокутне givens_decomposition_slater(), яке реалізує лише заселені орбіталі, замість повного квадратного розкладу, якого вимагає один лише OrbitalRotation. Це строго дешевший синтез (див. виграш наприкінці цього посібника).

Транспілерний прохід MergeSlaterDeterminantPreparation виявляє шаблон InitializeModes з наступним OrbitalRotation у FermionicCircuit і переписує його на вентилі PrepareSlaterDeterminant, тож пізніший етап синтезу автоматично підхоплює зменшений розклад. У симуляції це переписування не змінює стан: PrepareSlaterDeterminant працює за принципом перевір-потім-оберни (він перевіряє опорну заселеність, а потім застосовує обертання, так само як і два окремі вентилі), тож злиття відкриває дешевший синтез без зміни підготовленого стану.

У цьому посібнику кожну схему намальовано до і після проходу поруч: вхід ліворуч, а результат застосування проходу до нього праворуч. Це роблять два допоміжні засоби. merge запускає прохід на копії схеми (через її FermionicDAGCircuit) і перетворює результат назад у FermionicCircuit, який можна намалювати, а draw_merge малює обидві половини в єдиному малюнку до/після:

>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>>
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import InitializeModes, OrbitalRotation
>>> from qiskit_fermions.transpiler import FermionicCircuitToDAG
>>> from qiskit_fermions.transpiler.converters import FermionicDAGToCircuit
>>> from qiskit_fermions.transpiler.passes import MergeSlaterDeterminantPreparation
>>>
>>> def merge(circuit):
... dag = FermionicCircuitToDAG().run(circuit)
... merged = MergeSlaterDeterminantPreparation().run(dag)
... return FermionicDAGToCircuit().run(merged)
>>>
>>> def natural_size(circuit):
... # the figure size Qiskit picks for a circuit on its own (discard the probe figure)
... probe = circuit.draw("mpl")
... size = probe.get_size_inches()
... plt.close(probe)
... return size
>>>
>>> def draw_merge(circuit):
... merged = merge(circuit)
... # let Qiskit size each half naturally, then lay them out side by side
... (bw, bh), (aw, ah) = natural_size(circuit), natural_size(merged)
... fig, (before, after) = plt.subplots(1, 2, figsize=(bw + aw, max(bh, ah)))
... circuit.draw("mpl", ax=before)
... merged.draw("mpl", ax=after)
... before.set_title("before")
... after.set_title("after")
... return fig

Що зливається​

Прохід розпізнає три шаблони, усі прив'язані до блочно-спінової конвенції мод, яку використовують InitializeModes та OrbitalRotation. Для системи з norb просторовими орбіталями моди 0..norb – це спін-альфа сектор, а norb..2*norb – спін-бета сектор. Підготовка детермінанта Слейтера виконується для кожного спінового сектора окремо, оскільки кожен сектор має власну форму (електрони, орбіталі).

Повний регістр (або без спіну)​

Найпростіший шаблон – це InitializeModes, за яким одразу йде OrbitalRotation на тих самих модах. Це випадок без спіну (один детермінант по всіх модах), а також спінова схема, обертання якої охоплює обидва сектори одразу. Два вентилі зливаються в єдиний PrepareSlaterDeterminant:

>>> circuit = FermionicCircuit(4)
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(4)), circuit.modes)
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A mode initialization and rotation fusing into one Slater determinant preparation.

На спіновий сектор​

Це та сама форма, але обмежена однією спіновою половиною. Тут дві незалежні пари ініціалізація-потім-обертання, по одній на сектор, зливаються кожна окремо в PrepareSlaterDeterminant:

>>> circuit = FermionicCircuit(6) # norb = 3: alpha modes 0..3, beta modes 3..6
>>> circuit.append(InitializeModes([1, 1, 0]), circuit.modes[:3])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> circuit.append(InitializeModes([1, 0, 0]), circuit.modes[3:])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[3:])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

Two per-sector initialize-and-rotate pairs, each fusing into its own preparation.

Глобальна ініціалізація, обертання на кожен спін​

Це передбачає один InitializeModes на весь регістр (2 * norb), за яким ідуть два OrbitalRotation – по одному на кожну суцільну спінову половину. Прохід розбиває глобальну заселеність на межі секторів і видає два вентилі PrepareSlaterDeterminant. Два обертання можуть іти в будь-якому порядку.

Таку форму дає робочий процес локального унітарного кластерного Джастроу (LUCJ) (див. посібник LUCJ); користувач ставить InitializeModes (зазвичай опорний стан Хартрі-Фока, використовуючи from_hartree_fock()) на початок схеми та додає анзац UCJ, перші два спінові орбітальні обертання якого безпосередньо йдуть за ініціалізацією після декомпозиції анзаца.

>>> circuit = FermionicCircuit(6) # norb = 3, nelec = (2, 1)
>>> circuit.append(InitializeModes.from_hartree_fock(3, (2, 1)), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[3:])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A global initialization with two per-spin rotations fusing into two preparations.

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

>>> circuit = FermionicCircuit(6)
>>> circuit.append(InitializeModes.from_hartree_fock(3, (2, 1)), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A global initialization with a single per-spin rotation fusing into two preparations.

Що залишається незайманим​

Прохід навмисно консервативний. Він зливає лише тоді, коли два вентилі справді складаються в підготовку детермінанта Слейтера, а все інше копіює без змін. «Одразу за» розуміється над графом потоку даних схеми. OrbitalRotation зливається лише тоді, коли InitializeModes є його єдиним попередником по всіх його модах. На малюнках нижче до і після однакові: нічого не злилося.

Операція між ними​

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

>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.operators import FermionOperator, ann, cre
>>>
>>> circuit = FermionicCircuit(4)
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> number_op = FermionOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> circuit.append(Evolution(4, number_op, time=0.5), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(4)), circuit.modes)
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

An operation between the initialization and rotation blocks the merge.

Часткове обертання, що не є спіновою половиною​

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

>>> circuit = FermionicCircuit(4) # norb = 2: the only spin halves are modes [0, 1] and [2, 3]
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A partial rotation that is not a spin half is left unmerged.

Чому це важливо: виграш у синтезі​

Злиття виправдане, тому що PrepareSlaterDeterminant синтезується в набагато менше вентилів, ніж OrbitalRotation, який він поглинає. Детермінант з mm електронів на nn орбіталях потребує щонайбільше m(n−m)m(n-m) дво-кубітних обертань і жодних діагональних фазових вентилів, проти n(n−1)/2n(n-1)/2 обертань плюс nn фаз повного квадратного орбітального обертання.

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

Примітка

Пресет запускає MergeOrbitalRotations безпосередньо перед MergeSlaterDeterminantPreparation. Це важливо, коли за ініціалізацією йде серія послідовних вентилів OrbitalRotation. Раніший прохід спочатку згортає серію в єдине обертання, яке MergeSlaterDeterminantPreparation потім зводить з ініціалізацією в один PrepareSlaterDeterminant. Без такого порядку було б поглинуто лише перше обертання серії, а решта синтезувалася б повним квадратним розкладом.

>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> # a reproducible random 6 x 6 orbital rotation
>>> rng = np.random.default_rng(1)
>>> z = rng.standard_normal((6, 6)) + 1j * rng.standard_normal((6, 6))
>>> q, r = np.linalg.qr(z)
>>> rotation = q * (np.diagonal(r) / np.abs(np.diagonal(r)))
>>>
>>> circuit = FermionicCircuit(6)
>>> circuit.append(InitializeModes([1, 1, 0, 0, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(rotation), circuit.modes)
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> prepared = pm.run(circuit)
>>> print(dict(sorted(prepared.count_ops().items())))
{'x': 2, 'xx_plus_yy': 8}
>>> prepared.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The synthesized merged Slater determinant preparation.

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

>>> reference = FermionicCircuit(6)
>>> reference.append(OrbitalRotation(rotation), reference.modes)
>>> reference_prepared = pm.run(reference)
>>> print(dict(sorted(reference_prepared.count_ops().items())))
{'rz': 6, 'xx_plus_yy': 15}
>>> reference_prepared.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The synthesized bare orbital rotation, with its extra phase gates.

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

Два способи розібрати вентиль​

PrepareSlaterDeterminant можна розібрати двома різними способами, і вони не взаємозамінні:

decompose()​

Розгортає вентиль у його визначення: InitializeModes опорного стану, за яким іде повний OrbitalRotation. Це еквівалентно вентилю як оператору для будь-якого вхідного стану.

PrepareSlaterDeterminant.synthesis, що застосовується F2FSynthesis​

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

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

Для симуляції схеми за допомогою ffsim не потрібне ні те, ні інше: кожен вентиль застосовує весь свій унітарний оператор, тож схема з PrepareSlaterDeterminant симулюється як є та залишається на швидкому шляху для вентилів OrbitalRotation навколо нього. Розбір лише вентиля Слейтера за допомогою circuit.decompose("PrepareSlaterDeterminant") також залишає ці обертання цілими.

Дивись також

PrepareSlaterDeterminant, InitializeModes, OrbitalRotation, MergeOrbitalRotations, F2FSynthesis, FermionicSlaterGivensDecomposition, а також посібник LUCJ про робочий процес, що дає шаблон глобальної ініціалізації.