Транспіляція анзаца LUCJ
Концепції цього посібника доступні лише в Python API.
Локальний унітарний кластерний анзац Джастроу (LUCJ) — це компактна, апаратно ефективна параметризація корельованої електронної хвильової функції. Він є членом ширшого сімейства унітарних кластерних анзаців Джастроу (UCJ) і має вигляд
де — опорний стан (зазвичай Хартрі-Фока), кожен — орбітальне обертання, а кожен — діагональний кулонівський оператор
де — оператор числа частинок на просторовій орбіталі зі спіном .
ffsim будує оператор анзаца; цей пакет перетворює його на схему та спускає цю схему на кубіти. Цей посібник присвячений другій половині цього речення. Причина проходити цей шлях — саме спуск: власні гейти Qiskit у ffsim специфічні для перетворення Йордана-Вігнера, тоді як FermionicCircuit не несе жодних припущень щодо свого ферміон-кубітного кодування, тож той самий анзац можна синтезувати через будь-яке кодування, що підходить цільовому пристрою.
Отже, оператор анзаца надходить сюди готовим, а все нижче — це те, що з ним відбувається далі: на що розкладається гейт, як він спускається через Йордана-Вігнера, як узгоджується з картою зв'язності пристрою і чого ще потребував би спуск не за Йорданом-Вігнером. Щодо побудови, параметризації чи симуляції анзаца UCJ як такого, див. власні посібники ffsim.
Посібник про зв'язок з ffsim — як два пакети розподіляють роботу, і посібник з транспіляції — етапи конвеєра, використані нижче.
1. Отримай оператор LUCJ від ffsim
Анзац ініціалізується з амплітуд обчислення методом зв'язаних кластерів із однократними та дворазовими збудженнями (CCSD). Виконай обмежений метод Хартрі-Фока, а потім CCSD для молекули водню в базисі 6-31g, використовуючи PySCF для квантової хімії, а потім передай амплітуди до from_t_amplitudes() з ffsim. Вона виконує подвійну факторизацію амплітуд , щоб отримати діагональні кулонівські матриці та орбітальні обертання для кожного шару, і виводить необов'язкове фінальне орбітальне обертання з амплітуд . Кількість повторень — це те, що дає ця факторизація; n_reps її обрізає, жертвуючи точністю заради меншої глибини схеми.
>>> import ffsim
>>> import pyscf
>>> import pyscf.cc
>>>
>>> # build the molecule and run Hartree-Fock
>>> mol = pyscf.gto.Mole()
>>> mol.build(
... atom=[["H", (0, 0, 0)], ["H", (0, 0, 0.74)]],
... basis="6-31g",
... symmetry="Dooh",
... verbose=0,
... )
<pyscf.gto.mole.Mole object at ...>
>>> scf = pyscf.scf.RHF(mol).run()
>>>
>>> norb = scf.mo_coeff.shape[1]
>>> nelec = (mol.nelec[0], mol.nelec[1])
>>>
>>> # run CCSD for the t-amplitudes, then factorize them into an ansatz operator
>>> ccsd = pyscf.cc.CCSD(scf).run()
>>> t1, t2 = ccsd.t1, ccsd.t2
>>>
>>> ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(t2, t1=t1, n_reps=2)
Тут анзац перестає бути справою ffsim. ffsim пропонує тут варіанти, яких цей пакет не бачить і про які йому не потрібно знати, як-от варіаційно оптимізована («стиснена») факторизація за optimize=True або пакування вектора параметрів, яким керує варіаційний оптимізатор. Усі вони дають той самий вид оператора, і все нижче працює без змін з будь-яким із них.
2. Перетвори оператор на ферміонну схему
Гейт UCJ обгортає оператор і виражає його як схему над ферміонними модами. Гейт є чисто унітарним і не несе власного опорного стану, тож додай на початок гейт InitializeModes (побудований через from_hartree_fock()), щоб задати опорний стан Хартрі-Фока, до якого застосовується анзац.
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import InitializeModes, UCJ
>>>
>>> ansatz = UCJ(ucj_op)
>>>
>>> circuit = FermionicCircuit(2 * norb)
>>> circuit.append(InitializeModes.from_hartree_fock(norb, nelec), circuit.modes)
>>> circuit.append(ansatz, circuit.modes)
Декомпозиція схеми розкриває її анатомію, і саме це представлення використовує решта посібника. Гейт InitializeModes готує опорний детермінант, а кожен шар анзаца додає OrbitalRotation , потім (гейт Evolution діагонального кулонівського оператора ), потім , а наприкінці — фінальний OrbitalRotation. Орбітальні обертання діють по спінових секторах, тож кожне розміщується на alpha-модах 0..norb і beta-модах norb..2*norb незалежно.
>>> circuit.decompose().draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

Кожен гейт цієї декомпозиції досі ферміонний: OrbitalRotation та Evolution визначені на модах, і жодного кубітного чи паулієвого оператора ще не з'явилося. Саме це залишає кодування відкритим, і саме в такому стані анзац лишається, доки етап синтезу не обере кодування.
Кожен шар закінчується на , а наступний починається з , тож сусідні гейти OrbitalRotation можна об'єднати в одне обертання. Прохід транспіляції, що виконує таке злиття, — це запланована майбутня розробка.
3. Обери ферміон-кубітне кодування
Схема досі ферміонна, і саме в цій точці цей пакет виправдовує своє місце в робочому процесі. FermionicCircuit не несе припущень про те, як моди стають кубітами, тож кодування є вибором етапу синтезу, а не чимось вбудованим в анзац. Саме це додає цей пакет порівняно з власними гейтами Qiskit у ffsim, які специфічні для перетворення Йордана-Вігнера.
Лише для Йордана-Вігнера цей об'їзд не потрібен: ffsim постачає UCJOpSpinBalancedJW та його споріднені класи, які самостійно переводять оператор UCJ у кубітну схему. Причина вести анзац через ферміонну схему — усе інше, що може дати кодування. Наприклад, локальні кодування витрачають додаткові кубіти, щоб обмежити паулієву вагу кожного доданка, що використовують посібники з потокових множин для 1D і 2D, щоб двокубітна глибина кроку Троттера не залежала від розміру системи.
Конкретно для анзаца UCJ етап синтезу має охопити два види гейтів:
-
Діагональна кулонівська еволюція.
Evolutionуже не залежить від кодування:MapperFnEvolutionSynthesisприймає функцію-мапер, тож шари спускаються через будь-яке кодування, яке можна виразити такою функцією. Посібники з потокових множин показують, як її написати. -
Орбітальні обертання та підготовка детермінанта Слейтера. Ні
OrbitalRotation, ніPrepareSlaterDeterminant, в який зливаються початковийInitializeModesта обертання (див. нижче), не відображаються на кубіти як такі. ПрохідF2FSynthesisспершу розбиває їх у ферміонному просторі на гейтиEvolutionна одній або двох модах кожен (а для підготовки — ще й наInitializeModesопорного стану), тож будь-яке кодування, що вміє відобразитиEvolution, впорається з ними. За Йордана-Вігнера множники між сусідніми модами на кубітному етапі стають об'єктамиXXPlusYYGate. -
Ініціалізація. Плагін для
InitializeModesне параметризований кодуванням: він документує припущення про кодування в базисі заповнення з відображенням мода-кубіт один до одного і видає гейти безпосередньо в цьому базисі. Для іншого кодування потрібен власний плагін, і жодного поки немає.
Як транспілювати схеми UCJ за кодування не Йордана-Вігнера — відкрите дослідницьке питання, а цей пакет надає каркас, у межах якого можна розробити такий конвеєр. Решта цього посібника як приклад будує конвеєр транспілятора для кодування Йордана-Вігнера, еквівалентний тому, що створює generate_preset_jw_pass_manager().
4. Спусти його через Йордана-Вігнера
Спуск відбувається в F2QSynthesis, який працює подібно до HighLevelSynthesis у Qiskit: він обходить ферміонні інструкції та передає кожну плагіну, зареєстрованому для імені цього гейта. Його атрибут F2QSynthesis.methods містить один F2QSynthesisPlugin на ім'я гейта, тож побудова етапу означає вибір плагіна для кожного ферміонного гейта, що містить схема:
>>> from qiskit_fermions.mappers.library import jordan_wigner
>>> from qiskit_fermions.transpiler.passes import F2QSynthesis
>>> from qiskit_fermions.transpiler.passes.synthesis import (
... MapperFnEvolutionSynthesis,
... TrivialOccupationInitializeModesSynthesis,
... )
>>>
>>> synthesis = F2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(jordan_wigner)
>>> synthesis.methods["InitializeModes"] = TrivialOccupationInitializeModesSynthesis()
Призначення екземплярів таким чином — це також спосіб, у який плагін, написаний тобою самостійно, потрапляє в конвеєр без необхідності спершу його десь реєструвати.
Саме тут кодування остаточно визначається, двома способами, описаними в розділі 3: явно, передаючи jordan_wigner() до MapperFnEvolutionSynthesis, і неявно, через інший плагін, який уже його припускає.
Потім цей етап вставляється в MultiStagePassManager, етапи якого виконуються у фіксованому порядку:
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit.transpiler import generate_preset_pass_manager
>>> from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig
>>>
>>> from qiskit_fermions.transpiler import (
... FermionicCircuitToDAG,
... FermionicPassManager,
... QuantumDAGToCircuit,
... )
>>> from qiskit_fermions.transpiler.passes import (
... F2FSynthesis,
... HopEvolutionSynthesis,
... MergeOrbitalRotations,
... MergeSlaterDeterminantPreparation,
... TrivialF2QLayout,
... )
>>>
>>> pm = MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... optimization=FermionicPassManager(
... [
... MergeOrbitalRotations(),
... MergeSlaterDeterminantPreparation(),
... F2FSynthesis(
... {"OrbitalRotation": "Givens", "PrepareSlaterDeterminant": "Givens"}
... ),
... ]
... ),
... layout=FermionicPassManager(TrivialF2QLayout()),
... synthesis=synthesis,
... qubit=generate_preset_pass_manager(
... hls_config=HLSConfig(PauliEvolution=[HopEvolutionSynthesis(), "default"])
... ),
... output=QuantumDAGToCircuit(),
... )
Етап optimization виконується перед синтезом, на ферміонній схемі, і саме тому він корисний: він переписує гейти, поки вони ще ферміонні та не залежать від кодування, тож виграш переноситься на будь-яке кодування, що йде далі. MergeOrbitalRotations згортає кожну послідовність сусідніх гейтів OrbitalRotation в один, що важливо для анзаца UCJ, бо кожен шар закінчується на , а наступний починається з . Потім MergeSlaterDeterminantPreparation зливає початковий InitializeModes і обертання, що йде за ним, в єдиний PrepareSlaterDeterminant, який синтез спускає через дешевшу редуковану декомпозицію Слейтера замість повного квадратного обертання. Порядок має значення: спершу злиття обертань одразу після ініціалізації оголює єдине обертання, яке потім зливанням Слейтера стягується в неї. F2FSynthesis іде останнім, бо розбиває будь-яке обертання, що залишилося, а зробивши це раніше, він не залишив би двом проходам злиття нічого для об'єднання.
Решта етапів — службові. input та output перетворюють між схемами та DAG, layout призначає ферміонні моди кубітам (TrivialF2QLayout відображає моду i на кубіт i), а qubit — звичайний менеджер проходів Qiskit, який перебирає керування, коли нічого ферміонного не лишається.
Для запуску потрібен один підготовчий крок: складений гейт UCJ спершу треба декомпонувати на його примітивні гейти, щоб етап оптимізації бачив окремі обертання та еволюції.
>>> transpiled = pm.run(circuit.decompose())
>>> print(dict(sorted(transpiled.count_ops().items())))
{'rz': 14, 'rzz': 12, 'x': 2, 'xx_plus_yy': 28}
Без цільового пристрою це відображається на 2 * norb кубітів із припущенням про зв'язність усіх з усіма; орбітальні обертання синтезуються в об'єкти XXPlusYYGate, а діагональні кулонівські еволюції — в об'єкти RZZGate:
>>> transpiled.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
Цей конвеєр — саме те, що збирає generate_preset_jw_pass_manager(), тож на практиці користуйся пресетом, а етапи будуй вручну, коли потрібно змінити один із них:
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> preset = generate_preset_jw_pass_manager()
>>> preset.run(circuit.decompose()).count_ops() == transpiled.count_ops()
True
5. Узгодь топологію пристрою
Реальний пристрій має обмежену карту зв'язності кубітів, і анзац LUCJ розроблено так, щоб вона відповідала їй. Взаємодії з однаковим спіном (pairs_aa) утворюють два лінійні ланцюги, а взаємодії alpha-beta (pairs_ab) з'єднують їх мостами. generate_lucj_pass_manager() з ffsim будує кубітний конвеєр, що враховує пристрій, для цієї структури і повертає підмножину pairs_ab, яку апаратура справді може вмістити. Вставте цей конвеєр в етап qubit пресета, зберігаючи власний ферміон-кубітний синтез пакета:
>>> from ffsim.qiskit import generate_lucj_pass_manager
>>> from qiskit.providers.fake_provider import GenericBackendV2
>>> from qiskit.transpiler import CouplingMap
>>>
>>> # a heavy-hex device coupling map (any BackendV2 works, e.g. a real fake_provider backend)
>>> coupling_map = CouplingMap.from_heavy_hex(5)
>>> backend = GenericBackendV2(
... num_qubits=coupling_map.size(),
... basis_gates=["cp", "xx_plus_yy", "p", "x", "swap"],
... coupling_map=coupling_map,
... )
>>>
>>> # nearest-neighbor same-spin chain; let the pass manager choose the alpha-beta pairs
>>> pairs_aa = [(p, p + 1) for p in range(norb - 1)]
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> hls_config = HLSConfig(PauliEvolution=[HopEvolutionSynthesis(), "default"])
>>> pm.qubit, allowed_pairs_ab = generate_lucj_pass_manager(
... backend,
... norb,
... "heavy-hex",
... (pairs_aa, None),
... optimization_level=3,
... seed_transpiler=0,
... hls_config=hls_config,
... )
>>>
>>> # the alpha-beta interactions the heavy-hex connectivity can implement
>>> print(allowed_pairs_ab)
[(0, 0)]
У цьому й суть: етап qubit враховує пристрій, а етап ферміон-кубітного синтезу перед ним лишається власним для цього пакета, а отже замінним. Заміна етапу qubit справді прибирає те, що пресет там реєструє, а саме HopEvolutionSynthesis, який перетворює множники Гівенса орбітального обертання на об'єкти XXPlusYYGate. Передай його далі через hls_config, як вище, або базис пристрою доведеться досягати без нього.
Коли pm.qubit тепер встановлено на конвеєр, що враховує пристрій, запуск менеджера проходів розміщує схему на кубітах бекенда та маршрутизує її до карти зв'язності. Для необмеженого анзаца з кроку 1, діагональний кулонівський оператор якого все ще містить alpha-beta доданки, до яких апаратура не має прямого доступу, маршрутизатор мусить вставити багато гейтів SWAP, щоб їх з'єднати:
>>> naive = pm.run(circuit.decompose())
>>> naive.num_qubits # laid out on the full heavy-hex device register
57
>>> naive_swaps = naive.count_ops()["swap"]
>>> naive_swaps # many SWAPs to bridge the unreachable alpha-beta interactions
33
Обмеж анзац взаємодіями, реалізовними апаратно
Виправлення — передати allowed_pairs_ab назад у побудову анзаца через аргумент interaction_pairs функції from_t_amplitudes(), щоб діагональний кулонівський оператор містив лише alpha-beta доданки, які карта зв'язності може реалізувати напряму. Тоді анзац відповідає топології пристрою, і маршрутизатор майже не має його чіпати:
>>> restricted = UCJ(
... ffsim.UCJOpSpinBalanced.from_t_amplitudes(
... t2, t1=t1, n_reps=2, interaction_pairs=(pairs_aa, allowed_pairs_ab)
... )
... )
>>>
>>> circuit = FermionicCircuit(2 * norb)
>>> circuit.append(InitializeModes.from_hartree_fock(norb, nelec), circuit.modes)
>>> circuit.append(restricted, circuit.modes)
>>>
>>> transpiled = pm.run(circuit.decompose())
>>> restricted_swaps = transpiled.count_ops()["swap"]
>>> restricted_swaps # far fewer routing SWAPs than the unrestricted ansatz
4
Відображення лише активних кубітів (idle_wires=False) показує схему, обмежену двома спіновими ланцюгами та alpha-beta мостом, виражену в базисних гейтах пристрою. Розміщення та маршрутизація розкидають логічні моди по фізичних кубітах пристрою, тож передай wire_order, взятий з фінального розміщення схеми (final_index_layout() перелічує фізичний кубіт, на якому опинився кожен вхідний кубіт, у порядку вхідних кубітів), щоб намалювати лінії назад у вихідному порядку мод:
>>> wire_order = transpiled.layout.final_index_layout(filter_ancillas=False)
>>> transpiled.draw("mpl", idle_wires=False, fold=-1, wire_order=wire_order)
<Figure size ... with 1 Axes>

Точна кількість гейтів і глибина після розміщення залежать від проходів маршрутизації/оптимізації та обраного пристрою, тож тут їх не наведено. Головне — спільне проєктування: вираження анзаца ланцюгом pairs_aa найближчих сусідів і мостами pairs_ab, відфільтрованими за апаратурою, тримає синтезовану схему близькою до топології пристрою, мінімізуючи накладні витрати на маршрутизацію (вставлені гейти SWAP). Див. FermionicSlaterGivensDecomposition щодо споріднного вибору синтезу (minimize_2q_gate_count), який обмінює кількість двокубітних гейтів на глибину після маршрутизації.
Наступні кроки
-
Дізнайся, як працюють окремі гейти, у документації
qiskit_fermions.circuit.libraryта в посібнику зі ферміонних схем. -
Ознайомся з поясненням щодо операторів, щоб зрозуміти, як безпосередньо будувати ферміонні оператори та гамільтоніани.
-
Подивись, як ферміонна схема відображається на кубіти, у посібнику з транспіляції.
-
Прочитай документацію
F2QSynthesisпро інший спосіб налаштування етапу синтезу: словникconfig, що обирає плагіни за короткими іменами, під якими вони реєструються у своїх точках входуqiskit_fermions.transpiler.synthesis, а не за екземпляром. -
Прочитай посібник про зв'язок з ffsim, щоб зрозуміти, як два пакети розподіляють роботу і які протоколи дозволяють ffsim симулювати схеми цього пакета.
