Використання trim SQD для кращого відбору конфігурацій
Типова поведінка diagonalize_fermionic_hamiltonian полягає в діагоналізації кожного підвибраного пакета та збереженні найкращого результату. Trim SQD натомість розглядає пакети як кандидатів для відсіювання: кожен діагоналізується, обрізається до конфігурацій з найбільшою вагою, а ті, що вижили з усіх пакетів, об'єднуються й діагоналізуються разом. Саме енергія цієї об'єднаної діагоналізації повідомляється як результат ітерації, а її власний обрізаний вивід засіває наступну ітерацію.
Реалізація trim SQD ґрунтується на спільному дослідженні
Cleveland Clinic Foundation, RIKEN та IBM®. Якщо ти використовуєш цю функцію у
своєму дослідженні, також процитуй статтю. Цитування можна знайти у файлі
CITATION.bib за ключем цитування merz2026crossing.
Щоб дізнатися більше про код SQD, використаний у цьому прикладі, дивись навчальний посібник про Гамільтоніан хімічної системи.
Налаштуй молекулу
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "pyscf": "pyscf", "qiskit_addon_sqd": "qiskit-addon-sqd"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import numpy as np
import pyscf
import pyscf.mcscf
from qiskit_addon_sqd.counts import generate_bit_array_uniform
from qiskit_addon_sqd.fermion import diagonalize_fermionic_hamiltonian
from qiskit_addon_sqd.trim import TrimPolicy
# Build N2 molecule
mol = pyscf.gto.Mole()
mol.build(
atom=[["N", (0, 0, 0)], ["N", (1.0, 0, 0)]],
basis="6-31g",
symmetry="Dooh",
)
# Define active space
n_frozen = 2
active_space = range(n_frozen, mol.nao_nr())
# Get molecular integrals
scf = pyscf.scf.RHF(mol).run()
num_orbitals = len(active_space)
n_electrons = int(sum(scf.mo_occ[active_space]))
num_elec_a = (n_electrons + mol.spin) // 2
num_elec_b = (n_electrons - mol.spin) // 2
cas = pyscf.mcscf.CASCI(scf, num_orbitals, (num_elec_a, num_elec_b))
mo = cas.sort_mo(active_space, base=0)
hcore, nuclear_repulsion_energy = cas.get_h1cas(mo)
eri = pyscf.ao2mo.restore(1, cas.get_h2cas(mo), num_orbitals)
# Compute exact energy
exact_energy = cas.run().e_tot
# Create a seed to control randomness throughout this workflow
rng = np.random.default_rng(24)
# Generate random samples
bit_array = generate_bit_array_uniform(
10_000, num_orbitals * 2, rand_seed=rng
)
WARN: Unable to to identify input symmetry using original axes.
Different symmetry axes will be used.
converged SCF energy = -108.835236570774
CASCI E = -109.046671778080 E(CI) = -32.8155692383188 S^2 = 0.0000000
Запусти цикл
Одна ітерація trim SQD виконує таке:
-
Раунд відсіювання. Розбий бітові рядки на пакети. Діагоналізуй кожен пакет і залиш частку
trim_ratioйого CI-рядків для кожного спінового сектора, ранжованих за вагою. -
Об'єднаний раунд. Діагоналізуй об'єднані вцілілі рядки один раз. Це результат, який повідомляє ітерація, а її власні обрізані рядки засівають наступну ітерацію.
sci_solver циклу виконує обидва раунди та за замовчуванням використовує розв'язувач PySCF цього пакета.
Увімкни trim SQD, передавши TrimPolicy так:
result = diagonalize_fermionic_hamiltonian(
hcore,
eri,
bit_array,
samples_per_batch=300,
norb=num_orbitals,
nelec=(num_elec_a, num_elec_b),
num_batches=5,
max_iterations=5,
policy=TrimPolicy(trim_ratio=0.5, max_strings_per_trim=250),
symmetrize_spin=True,
max_dim=250,
seed=rng,
)
sci_state = result.sci_state
print(
f"Merged subspace: {len(sci_state.ci_strs_a)} x {len(sci_state.ci_strs_b)} CI strings"
)
print(f"Exact energy: {exact_energy}")
print(f"Estimated energy: {result.energy + nuclear_repulsion_energy}")
Merged subspace: 148 x 148 CI strings
Exact energy: -109.04667177808024
Estimated energy: -109.034369147063
Перевір, чи допомагає відсіювання
Відсіювання дає найбільше тоді, коли кількість вибраних конфігурацій значно перевищує те, що можна діагоналізувати за раз, тож ранжування більшого пулу справді знаходить конфігурації, які пропустив би окремий пакет. Воно дає найменше тоді, коли підпростір доступного розміру вже охоплює більшу частину хвильової функції.
Відсіювання залежить від того, що пакети відрізняються один від одного. TrimPolicy забезпечує це, змінюючи поведінку diagonalize_fermionic_hamiltonian так, щоб вибрати один пул з samples_per_batch * num_batches бітових рядків і розподілити його між пакетами, тож жоден бітовий рядок не з'являється більш ніж в одному пакеті.
Обмеж підпростір
Підпростір тут натягнуто на декартів добуток масиву CI-рядків зі спіном альфа та масиву CI-рядків зі спіном бета.
Обрізання ранжує конфігурації, але перенесення складається з двох списків рядків для кожного спіну. Збереження 150 конфігурацій підпростору з найбільшою вагою зазвичай охоплює близько 150 різних альфа-рядків і 150 різних бета-рядків, добуток яких натягує приблизно 22 500 конфігурацій, а не 150. Тому підпростір, переданий на наступну ітерацію, загалом значно більший за множину, яку ранжували, і він зростає квадратично відносно того, що ти зберігаєш.
Обрізання та перенесення обмежуються окремо, бо вони відбуваються в різних точках: обрізання зменшує кожен пакет раунду відсіювання, тоді як перенесення є тим, що переходить на наступну ітерацію.
Обмежують його чотири аргументи, і корисно задати принаймні один:
-
trim_ratioце частка рядків кожного пакета для кожного спінового сектора, що переживають відсіювання. Менші значення зменшують об'єднаний підпростір, але відкидають конфігурації, які наступна ітерація має віднайти повторно через вибірку. -
max_strings_per_trimобмежує кількість рядків, яку кожен пакет вносить для кожного сектора, незалежно від співвідношення. Корисно зsymmetrize_spin, який об'єднує обидва спінові сектори в один список і тим самим приблизно подвоює те, що пропонує кожен пакет. Це обмеження на пакет: об'єднаний підпростір може містити доnum_batchesразів більше рядків, за вирахуванням того, що пакети мали спільного, а на практиці цього небагато. -
carryover_threshold,carryover_ratioтаmax_carryoverобмежують рядки, що засівають наступну ітерацію, і застосовуються до об'єднаного підпростору загалом, а не до кожного пакета.carryover_thresholdце поріг амплітуди, який використовуєStandardPolicy, з тим самим значенням за замовчуванням; два інші обмежують кількість, чого сам лише поріг не робить. -
max_dimу самоїdiagonalize_fermionic_hamiltonianобмежує кожен спіновий сектор кожного підпростору, який діагоналізує цикл, включно з об'єднаним. Він діє як страховка й застосовується до всього, що повертає політика, після того як перенесення об'єднано зі свіжими вибірками.
Запуск вище задає max_strings_per_trim=250 і max_dim=250 разом, тож ані те, що вносить кожен пакет, ані підпростір, побудований з нього, не можуть вийти з-під контролю.
Використання зовнішнього eigensolver
Ти можеш використовувати trim SQD із зовнішнім eigensolver, передавши аргумент sci_solver:
from sbd.sbd_solver import create_sbd_solver
result = diagonalize_fermionic_hamiltonian(
hcore,
eri,
bit_array,
samples_per_batch=300,
norb=num_orbitals,
nelec=(num_elec_a, num_elec_b),
num_batches=5,
policy=TrimPolicy(trim_ratio=0.1),
sci_solver=create_sbd_solver(sbd_config={"method": 0, "eps": 1e-10}),
seed=rng,
)
Попередній фрагмент вимагає, щоб пакет sbd-eigensolver було встановлено з PyPI.
Розподіл роботи
З TrimPolicy цикл викликає sci_solver двічі за ітерацію: один раз з усіма пакетами й один раз з об'єднаним підпростором. Обидва виклики проходять через звичайний інтерфейс sci_solver, який багатопроцесна підтримка цього пакета викликає колективно на кожному MPI-ранзі.
Потенційно доступні два рівні паралелізму, і вони незалежні:
-
Усередині діагоналізації. Розв'язувач на кшталт SBD може розподілити одну діагоналізацію між рангами.
-
Між пакетами. Розв'язувач міг би, в принципі, діагоналізувати пакети одночасно.
Маючи набір пакетів, розв'язувач визначає, як делегувати роботу своїм робочим процесам.