Генерація схем SqDRIFT
У вибірковій квантовій діагоналізації (SQD) потрібно обрати анзац, з якого семплуватимуться бітові рядки. Варіант SqDRIFT натомість використовує ансамбль схем часової еволюції, побудованих безпосередньо з цільового гамільтоніана. Це досягається вибіркою менших операторів часової еволюції з гамільтоніана на основі його коефіцієнтів, що відомо як метод тротеризації qDRIFT.
Цей вступний посібник показує, як згенерувати ансамбль таких рандомізованих схем.
1. Налаштування гамільтоніана
Для цілей цього посібника завантаж гамільтоніан електронної структури N2 з файлу FCIDUMP. Існують інші способи побудови FermionOperator. Обов'язково ознайомся з його документацією, а також з qiskit_fermions.operators.library.
- PYTHON
- C
>>> from qiskit_fermions.operators.library import FCIDump
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> fcidump = FCIDump.from_file("docs/guides/n2.fcidump")
>>> num_modes = 2 * fcidump.norb
>>> hamil = FermionOperator.from_fcidump(fcidump)
#include <qiskit_fermions.h>
QfFCIDump* fcidump = NULL;
qf_fcidump_from_file("docs/guides/n2.fcidump", &fcidump);
QfFermionOperator* hamil = qf_ferm_op_from_fcidump(fcidump);
uint32_t num_modes = 2 * qf_fcidump_norb(fcidump);
2. Групування членів гамільтоніана
Скористайся численними симетріями, наявними в гамільтоніані електронної структури, згрупувавши пов'язані члени з однаковими коефіцієнтами. Ця дія змінює розподіл коефіцієнтів оператора, з якого семплує протокол qDRIFT, але не впливає на його гарантії збіжності. Принципово, групування членів, пов'язаних симетрією, призводить до сприятливого скорочення членів Паулі, що в підсумку дає меншу глибину схеми під час часової еволюції стану під їхньою дією.
Модуль qiskit_fermions.operators.terms.grouping надає допоміжні функції для групування членів оператора. Це докладніше пояснено в цьому посібнику.
Реалізація group_terms_by_electronic_structure() припускає, що члени гамільтоніана мають нормальний порядок!
- PYTHON
- C
>>> from qiskit_fermions.operators.terms.grouping import group_terms_by_electronic_structure
>>> from qiskit_fermions.operators.terms.ordering import canonical_order
>>>
>>> canon = canonical_order(hamil.normal_ordered().simplify(atol=1e-16))
>>> exit_code = group_terms_by_electronic_structure(canon, num_modes, two_body_physicist_order=False)
>>> assert exit_code is None
>>> print(canon.groups) # the groups attribute now contains some list of group indices
[0, ...]
QfFermionOperator* normal = qf_ferm_op_normal_ordered(hamil, NULL);
QfFermionOperator* simplified = qf_ferm_op_simplify(normal, 1e-16);
QfFermionOperator* canon = qf_ferm_op_canonical_order(simplified);
QfExitCode exit = qf_ferm_op_group_terms_by_electronic_structure(canon, num_modes, false);
assert(exit == QfExitCode_Success);
qf_ferm_op_free(normal);
qf_ferm_op_free(simplified);
Повний гамільтоніан електронної структури містить певні члени, включення яких у схему часової еволюції не впливає на отримувані бітові рядки і, отже, лише збільшує накладні витрати на семплування. Тому рекомендується відфільтрувати такі члени з гамільтоніана на цьому етапі, перш ніж будувати вентиль Evolution на наступному кроці.
Члени, що відповідають цьому опису, — це ті, що діагональні в базисі чисел заповнення, тобто добутки операторів числа (). Сюди входять стала зсуву енергії, часова еволюція якої лише вводить глобальну фазу в схему, окремі оператори числа, часова еволюція яких зводиться до однокубітних Z-обертань, а також добутки вищого порядку, такі як . Жоден із них не може змінити заповнення моди, тож їх відкидання не втрачає жодного збудженого вмісту. На відміну від відхилення для кожного добору на кроці 5, ця фільтрація відбувається один раз, на самому гамільтоніані, тож розподіл коефіцієнтів, з якого семплує протокол, перебудовується узгоджено з відфільтрованого оператора, а не перенормовується посеред добору.
Функція filter_diagonal_terms() видаляє такі члени з оператора на місці:
- PYTHON
- C
>>> from qiskit_fermions.operators.terms.filtering import filter_diagonal_terms
>>>
>>> filter_diagonal_terms(canon)
qf_ferm_op_filter_diagonal_terms(canon);
Відфільтрувати тут, один раз, значно дешевше, ніж фільтрувати багаторазово. FermionicQDrift запускається один раз для кожної транспільованої схеми, тож фільтрація гамільтоніана на початку, а не під час кожного виклику, дозволяє уникнути повторної роботи для кожної згенерованої з нього схеми.
З тієї самої причини тут варто впорядкувати члени гамільтоніана за індексом групи. Індекси груп — це позначка для кожного члена, яка нічого не каже про те, де ці члени розташовані, тож члени групи загалом розкидані по всьому оператору: застосоване вище групування призначає індекси в порядку першої появи, через що другий член групи опиняється далеко від першого.
FermionicQDrift семплує цілі групи та шукає кожну вибрану групу за допомогою split_out_groups(). Пошук групи серед розкиданих членів означає сканування всіх із них, тож цей пошук коштує однаково, чи вибрано одну групу, чи сорок. group_order() збирає кожну групу в один неперервний відрізок, після чого пошук стає бінарним пошуком за межами груп і коштує стільки, скільки коштують вибрані групи, а не наявні члени. Сам пошук стає драматично дешевшим; скільки це заощадить у конкретній транспіляції, залежить від того, що ще робить конвеєр, оскільки після усунення сканування домінують відображення та синтез:
- PYTHON
- C
>>> from qiskit_fermions.operators.terms.ordering import group_order
>>>
>>> canon = group_order(canon)
>>> print(canon.groups[:4])
[0, 0, ...]
QfFermionOperator* grouped = qf_ferm_op_group_order(canon);
Це переупорядковує члени без зміни значення оператора і потрібно лише один раз, незалежно від того, скільки рандомізацій схеми з нього добирається.
3. Підготовка схеми часової еволюції
Підготуй схему часової еволюції гамільтоніана та базову схему, з якої добиратимуться зразки. qiskit_fermions.circuit.library містить усі потрібні для цього компоненти відповідно до конвенцій Qiskit.
Протокол qDRIFT — це метод синтезу FermionicQDrift, а вентиль Evolution несе його як свій synthesis. Метод вводить кілька параметрів, якими можна налаштувати схеми, що генеруються:
-
Довжина кожної схеми в термінах груп збуджень:
num_groups -
За бажанням, початкове значення генератора випадкових чисел:
rng -
За бажанням, попередньо обчислені ваги семплування, якщо сама транспіляція стає вузьким місцем:
weights(див. (Необов'язково) Попереднє обчислення ваг семплування нижче)
Оскільки метод утримується вентилем, усе, що описує оператор вентиля, наприклад ці ваги, розташоване поруч із цим оператором.
- PYTHON
- C
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.circuit.library.synthesis import FermionicQDrift
>>>
>>> time = 1.0 # you can choose a desired scaling factor here
>>> num_groups = 10
>>> qdrift = FermionicQDrift(num_groups, rng=19)
>>> evo_gate = Evolution(num_modes, canon, time, synthesis=qdrift)
>>>
>>> circ = FermionicCircuit(num_modes)
>>> circ.append(evo_gate, circ.modes)
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions.
Цей приклад не ініціалізує ферміонні моди частинками і не вимірює їхній кінцевий стан.
4. Транспіляція схеми
Модуль qiskit_fermions.transpiler інтегрується безпосередньо з конвеєром транспіляції Qiskit, дозволяючи безпосередньо транспілювати побудовану вище FermionicCircuit у QuantumCircuit.
Використай відображення ферміонів на кубіти jordan_wigner(), щоб перетворити гамільтоніан, виражений через ферміони, на вираз через рядки Паулі. Це можна зробити безпосередньо в межах процесу транспіляції за допомогою проходу транспіляції F2QSynthesis. Використай generate_preset_jw_pass_manager(), щоб побудувати FermionicStagedPassManager, який гарантує узгоджене використання кодування Йордана—Вігнера для всіх інструкцій схеми.
Принципово додай прохід Decompose з Qiskit на етап optimization конвеєра транспіляції. Він розкладає вентиль Evolution за допомогою методу синтезу, який несе вентиль, тобто методу qDRIFT, підготовленого вище. Це гарантує, що схема не використовує часову еволюцію всього гамільтоніана, глибина якої перевищила б можливості наявного квантового обладнання.
Натомість він відбирає фіксовану кількість groups членів гамільтоніана для кожної схеми щоразу, коли схема транспілюється. Завдяки цьому можна згенерувати кілька рандомізацій схеми, як цього вимагає алгоритм SqDRIFT, багаторазово запускаючи конвеєр транспіляції. Кількість схем для генерації — num_sqdrift_randomizations.
- PYTHON
- C
>>> from qiskit.transpiler.passes import Decompose
>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> pm.optimization = FermionicPassManager([Decompose(["Evolution"])])
>>>
>>> num_sqdrift_randomizations = 10
>>> sqdrift_circuits = [
... pm.run(circ) for _ in range(num_sqdrift_randomizations)
... ]
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions, which we therefore also cannot transpile
// via this API.
Попередні приклади фіксують початкове значення rng методу синтезу FermionicQDrift, щоб послідовність рандомізацій можна було відтворити.
Той самий конвеєр приймає метод qDRIFT ще у двох місцях. Прохід F2FSynthesis приймає один метод для кожного вентиля Evolution за іменем, F2FSynthesis({"Evolution": FermionicQDrift(num_groups)}), чого достатньо, коли жоден вентиль не містить нічого специфічного для свого оператора. Наступний крок використовує третє місце — прохід QDriftTrotterization.
5. Фільтрація вибраних збуджень за заповненням
Окрім діагональних членів, відфільтрованих на попередньому кроці, вибране збудження може діяти повністю в межах набору мод, заповнення яких уже зафіксоване (усі заповнені або всі незаповнені), де воно не може перемістити частинку з одного боку на інший. Таке збудження нічого не повідомляє про вибрані бітові рядки, тож слот, витрачений на нього, витрачений даремно. Прохід QDriftTrotterization, який обгортає той самий метод FermionicQDrift, відхиляє такі збудження в міру їх вибору і добирає заміну, так що кожен із num_groups слотів результуючої схеми несе збудження, яке зв'язує обидві сторони.
Ця фільтрація не безкоштовна, і саме тому вона є окремим проходом. Вона зберігає бюджет num_groups вибраних слотів, а не глибину схеми: оскільки цей бюджет фіксований, відхилене збудження замінюється, а не відкидається, і дешеве поступається збудженню зі зв'язком, синтез якого коштує дорожче. Очікуй, що відфільтрована схема буде глибшою за нефільтровану, вибрану з того самого початкового значення.
Перенормування добору за прийнятими збудженнями змінює розподіл, з якого семплує протокол qDRIFT, і, на відміну від групування на кроці 2, воно також порушує гарантію збіжності протоколу: вибраний добуток більше не усереднюється до еволюції під дією переданого гамільтоніана. Кожне збережене збудження зрештою зважується на обернену величину ймовірності прийняття, відхилені не роблять жодного внеску, і ні сумарна величина коефіцієнтів, ні час еволюції на вентиль не коригуються для компенсації.
Правило прийняття також є завищеним наближенням. Воно порівнює лише носії мод, тож не містить інформації про амплітуди, а відстежувані набори лише зростають: збудження, обмежене зайнятим набором, справді інертне на добутковому стані, але те саме міркування слабшає для пізніших виборів, коли раніше прийняті збудження заплутали стан і позначили більшість мод як «невизначені».
Використовуй фільтрацію для процесу семплування бітових рядків, який описує цей посібник, де схеми подають дані в постобробку SQD і важливі лише вибрані бітові рядки. Не використовуй її, якщо маєш намір оцінювати очікуване значення за цими схемами або покладатися на межу похибки qDRIFT у будь-який інший спосіб: такі результати несуть зміщення, яке прохід не коригує.
Ця фільтрація має знати, які моди спочатку заповнені. Тому вона потребує вентиля InitializeModes перед вентилями Evolution у схемі; InitializeModes.from_hartree_fock() — зручний спосіб його побудувати. Додай такий тут для опорного стану Хартрі—Фока N2 (сім альфа- і сім бета-електронів на 14 просторових орбіталях) і порівняй вибрані збудження з фільтрацією та без неї:
- PYTHON
- C
>>> from qiskit_fermions.circuit.library import InitializeModes
>>> from qiskit_fermions.transpiler.passes import F2FSynthesis, QDriftTrotterization
>>>
>>> init = InitializeModes.from_hartree_fock(fcidump.norb, (7, 7))
>>>
>>> hf_circ = FermionicCircuit(num_modes)
>>> hf_circ.append(init, hf_circ.modes)
>>> hf_circ.append(Evolution(num_modes, canon, time), hf_circ.modes)
>>>
>>> num_groups = 5
>>> qdrift_unfiltered = F2FSynthesis(
... {"Evolution": FermionicQDrift(num_groups, rng=3480)}
... )
>>> qdrift_trivial = QDriftTrotterization(
... FermionicQDrift(num_groups, rng=3480)
... )
>>>
>>> for instruction in FermionicPassManager(qdrift_unfiltered).run(hf_circ)._inner.data:
... if instruction.operation.name == "Evolution":
... print(sorted(instruction.operation.operator.get_support()))
[2, 4]
[41, 45, 52]
[15, 45, 55]
[41, 52, 53]
[10, 16, 37, 38]
>>>
>>> for instruction in FermionicPassManager(qdrift_trivial).run(hf_circ)._inner.data:
... if instruction.operation.name == "Evolution":
... print(sorted(instruction.operation.operator.get_support()))
[0, 1, 6, 7]
[0, 1, 28, 29]
[4, 13, 55]
[13, 20, 40, 41]
[0, 1, 28, 29]
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions, which we therefore also cannot transpile
// via this API.
Жодне зі збуджень, вибраних без фільтрації, взагалі не торкається зайнятого набору (0-6 і 28-34), тож жодне з них не може перемістити частинку між зайнятою та незайнятою модою. З фільтрацією всі п'ять відхилено і замінено збудженнями, які таки зв'язують зайняту моду з незайнятою. Наприклад, перше прийняте збудження [0, 1, 6, 7] переміщує частинку між зайнятими модами 0, 1 і 6 та незайнятою модою 7.
Щойно збудження прийнято, кожна мода в його носії стає «невизначеною» і, отже, придатною виконувати будь-яку роль для пізніших зразків, тож набори зайнятих і незайнятих мод продовжують рости в міру прийняття нових збуджень. Саме тому друге збудження, [0, 1, 28, 29], є прийнятним. Усі його чотири моди належать до спочатку зайнятих, тож воно не зв'язується з жодною спочатку незайнятою модою. Його прийнято лише тому, що моди 0 і 1 стали невизначеними (а отже, придатними бути «незайнятою» стороною зв'язку), коли першого збудження торкнулося до них.
Це зростання робить правило завищеним наближенням, а не перевіркою: у міру того як більше мод стають невизначеними, відхиляється менше виборів, і фільтрація найбільше впливає на розподіл семплування в найперших виборах.
Щоб оцінити, наскільки фільтрація змістила розподіл, прочитай лічильники відхилень, які прохід записує в метаданих схеми. Зберігається один запис на кожен відфільтрований вентиль Evolution, а відношення прийнятих до загальної кількості оцінює ймовірність прийняття, тобто множник, на який збережені збудження були завищені за вагою:
- PYTHON
- C
>>> # a freshly seeded pass, so the counts do not depend on earlier draws
>>> qdrift_counted = QDriftTrotterization(
... FermionicQDrift(num_groups, rng=3480)
... )
>>> filtered = FermionicPassManager(qdrift_counted).run(hf_circ)
>>> discarded = filtered.metadata.get("filter_trivial.discarded")
>>> emitted = filtered.metadata.get("filter_trivial.emitted")
>>> print(f"{emitted[0]} kept, {discarded[0]} discarded")
5 kept, 7 discarded
>>> print(f"acceptance probability: {emitted[0] / (emitted[0] + discarded[0]):.2f}")
acceptance probability: 0.42
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions, which we therefore also cannot transpile
// via this API.
Для фільтрації потрібна інформація про заповнення, за якою фільтрувати. Вентиль PrepareSlaterDeterminant задає її так само, як і InitializeModes, а будь-який OrbitalRotation позначає кожну моду, на яку діє, як «невизначену». Без жодного з них або коли всі задані моди опиняються з одного боку (усі моди заповнені або всі незаповнені), прохід видає UserWarning і залишає семплування нефільтрованим для цього вентиля Evolution. На гамільтоніані, чиї решта членів більше не можуть зв'язати два відстежувані набори, прохід натомість викликає RuntimeError після QDriftTrotterization.MAX_SAMPLE_RETRIES послідовних відхилень.
(Необов'язково) Попереднє обчислення ваг семплування
Вдавайся до цього, лише якщо сама транспіляція є вузьким місцем. Це нічого не змінює в отримуваних схемах.
FermionicQDrift вибирає кожну групу з імовірністю, пропорційною величині її коефіцієнта, і виводить ці величини з гамільтоніана під час кожного виклику: одне значення на член гамільтоніана, зведене до одного на групу. Оскільки метод не зберігає стану, він повторює це зведення для кожної рандомізації, хоча результат щоразу однаковий. Аргумент weights дозволяє обчислити його один раз за допомогою group_coeff_means() і передати результат, виносячи витрати з циклу й залишаючи метод вільним від будь-якого кешованого стану, залежного від гамільтоніана.
Це відповідник впорядкування членів на кроці 2. Те виносить із циклу пошук групи, а це — зведення ваг, і кожне помітне лише тією мірою, якою відсутнє інше, тож варто застосувати обидва разом. Оскільки canon уже було переупорядковано вище, вони поєднуються безпосередньо.
Ваги описують групи одного конкретного оператора, тож вони належать вентилю Evolution цього оператора: побудуй метод для цього вентиля і передай його як synthesis вентиля, точно як на кроці 3.
- PYTHON
- C
>>> from qiskit_fermions.operators.terms.grouping import group_coeff_means
>>>
>>> weights = group_coeff_means(canon) # `canon` is group-ordered, from step 2
>>> qdrift_weighted = FermionicQDrift(10, rng=19, weights=weights)
>>> weighted_gate = Evolution(num_modes, canon, time, synthesis=qdrift_weighted)
>>>
>>> weighted_circ = FermionicCircuit(num_modes)
>>> weighted_circ.append(weighted_gate, weighted_circ.modes)
>>>
>>> weighted_circuits = [
... pm.run(weighted_circ) for _ in range(num_sqdrift_randomizations)
... ]
>>> len(weighted_circuits)
10
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions, which we therefore also cannot transpile
// via this API.
Оскільки це саме ті ваги, які обчислив би прохід, схеми такі самі, як і раніше; зникло лише повторне зведення.
Очікується один запис на групу, а не на член, щойно оператор несе групи, оскільки згрупований оператор семплується груповим чином. Записи мають бути невід'ємними: вага — це величина, адже протокол qDRIFT розкладає з додатними , а знак коефіцієнта належить , де прохід зчитує його безпосередньо з оператора.
Ваги — це абсолютні величини, а не відносні уподобання. Їхня сума також фіксує час еволюції, тож множення кожного запису на семплує так само, але еволюціонує впродовж замість . Тому передавання чогось іншого, ніж власні величини коефіцієнтів гамільтоніана, змінює те, яку еволюцію наближає ансамбль, і саме тому це інструмент продуктивності, а не спосіб налаштування семплування.
Оскільки ваги належать вентилю, чий оператор вони описують, схема може містити кілька вентилів Evolution, кожен із вагами власного оператора. Прохід QDriftTrotterization з кроку 5 враховує їх так само: він добирає вентиль за допомогою FermionicQDrift, який несе вентиль, і повертається до того, з яким його було створено, для вентилів, що його не несуть. Інший вид методу синтезу на вентилі не є твердженням про qDRIFT, тож прохід його ігнорує. Передавання методу проходу за іменем вентиля, як у F2FSynthesis({"Evolution": ...}), застосовує один метод до кожного вентиля Evolution, тож передані там ваги мають сенс лише для схеми з одним таким вентилем. Довжина ваг перевіряється щодо кожного вентиля, до якого вони застосовуються.
(Необов'язково) Оптимізація індексації ферміонних мод
Можна додати до конвеєра транспіляції додатковий крок оптимізації, який мінімізує відстань між носіями ферміонних збуджень шляхом перепозначення індексів ферміонних мод. Цю оптимізацію було запроваджено в статті SqDRIFT, і вона реалізована в build_excitation_span_minimization_model(). Її легко вставити в конвеєр транспілятора за допомогою проходу RelabelModes:
- PYTHON
- C
>>> from pyomo.environ import SolverFactory
>>> from qiskit_fermions.transpiler.passes import RelabelModes
>>>
>>> solver = SolverFactory("appsi_highs")
>>> solver.options["time_limit"] = 10
>>>
>>> qdrift = F2FSynthesis({"Evolution": FermionicQDrift(5, rng=19)})
>>> relabel = RelabelModes(solver=solver)
>>>
>>> pm.optimization = FermionicPassManager([qdrift, relabel])
>>>
>>> relabeled_circ = pm.run(circ)
>>> # if the automatic mode relabeling was successful, the circuit's
>>> # metadata will contain the mode `permutation` information
// WARNING: This feature is not available via the C API.
Використання автоматичної оптимізації всередині RelabelModes (яка спирається на build_excitation_span_minimization_model()) потребує необов'язкової залежності, якою керує HAS_PYOMO.
Перепозначення змінює те, яку моду несе даний кубіт, тож діагоналізація в підпросторі отримує правильні бітові рядки лише за умови врахування цього. Де саме відбувається врахування, залежить від того, де ти вимірюєш.
Додай вимірювання до FermionicCircuit за допомогою FermionicCircuit.measure_all() до транспіляції, і робити нічого не потрібно: кожне вимірювання називає моду, яку зчитує, тож перепозначення переносить його, а вибрані бітові рядки індексуються початковою модою.
Якщо натомість вимірювати транспільовану QuantumCircuit, бітові рядки треба постобробити за допомогою permutation, записаної в метаданих схеми. Рецепт дивись у документації RelabelModes.
Наступні кроки
Тепер, коли ти успішно згенерував ансамбль схем, можна семплувати з них бітові рядки. Для цього схеми потрібно виконати на обладнанні. Докладні інструкції дивись у документації Qiskit.
Щойно вибірки бітових рядків отримано, їх можна використати разом із коефіцієнтами гамільтоніана для постобробки SQD, як пояснено в навчальних посібниках з надбудови SQD.