Aproveite o trim SQD para melhorar a seleção de configurações
O comportamento padrão de diagonalize_fermionic_hamiltonian é diagonalizar cada lote subamostrado e manter o melhor resultado. Em vez disso, o Trim SQD trata os lotes como candidatos a serem triados: cada um é diagonalizado, aparado até as configurações de maior peso, e os sobreviventes de todos os lotes são unidos e diagonalizados juntos. Essa diagonalização unida é a que fornece a energia reportada pela iteração, e sua própria saída aparada alimenta a próxima iteração.
A implementação do trim SQD é baseada em uma pesquisa conjunta da
Cleveland Clinic Foundation, do RIKEN e da IBM®. Se você usar este recurso em
sua pesquisa, cite também o artigo. Você encontra a citação no
arquivo CITATION.bib, com a chave de citação merz2026crossing.
Para mais detalhes sobre o código SQD usado neste exemplo, consulte o tutorial do Hamiltoniano químico.
Configure a molécula
# 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
Execute o laço
Uma iteração do trim SQD faz o seguinte:
-
Rodada de triagem. Divida as bitstrings em lotes. Diagonalize cada lote e mantenha a fração
trim_ratiode suas strings CI, por setor de spin, ordenadas por peso. -
Rodada unida. Diagonalize uma vez os sobreviventes unidos. Este é o resultado que a iteração reporta, e suas próprias strings aparadas alimentam a próxima iteração.
O sci_solver do laço executa as duas rodadas e, por padrão, usa o solver PySCF deste pacote.
Ative o trim SQD passando o TrimPolicy da seguinte forma:
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
Verifique se a triagem está ajudando
A triagem tem mais a oferecer quando as configurações amostradas superam em muito o que pode ser diagonalizado de uma só vez, de modo que ranquear um conjunto maior realmente encontra configurações que um único lote teria perdido. Ela tem menos a oferecer quando um subespaço de tamanho viável já captura a maior parte da função de onda.
A triagem depende de os lotes serem diferentes entre si. O TrimPolicy cuida disso modificando o comportamento de diagonalize_fermionic_hamiltonian para sortear um conjunto de samples_per_batch * num_batches bitstrings e particioná-lo entre os lotes, de modo que nenhuma bitstring apareça em mais de um.
Limite o subespaço
Um subespaço aqui é gerado pelo produto cartesiano de um array de strings CI de spin alfa e um de spin beta.
O trim ranqueia configurações, mas o que é transportado são duas listas de strings por spin. Manter as 150 configurações de maior peso de um subespaço normalmente envolve cerca de 150 strings alfa distintas e 150 strings beta distintas, cujo produto gera aproximadamente 22.500 configurações, e não 150. Assim, o subespaço passado para a próxima iteração costuma ser muito maior que o conjunto que foi ranqueado, e cresce quadraticamente com o que você mantém.
O trim e o transporte são limitados separadamente, porque ocorrem em momentos diferentes: o trim reduz cada lote da rodada de triagem, enquanto o transporte é o que passa para a próxima iteração.
Quatro argumentos o limitam, e é útil definir pelo menos um:
-
trim_ratioé a fração das strings de cada lote, por setor de spin, que sobrevive à triagem. Valores menores reduzem o subespaço unido, mas descartam configurações que a próxima iteração precisa redescobrir por amostragem. -
max_strings_per_trimlimita as strings que cada lote contribui, por setor, independentemente da razão. É útil comsymmetrize_spin, que une os dois setores de spin em uma única lista e, portanto, dobra aproximadamente o que cada lote oferece. Esse limite é por lote: o subespaço unido pode conter aténum_batchesvezes mais strings, menos o que os lotes tinham em comum, o que na prática não é muito. -
carryover_threshold,carryover_ratioemax_carryoverlimitam as strings que alimentam a próxima iteração, aplicados ao subespaço unido como um todo, e não por lote.carryover_thresholdé o corte de amplitude queStandardPolicyusa, com o mesmo valor padrão; os outros dois limitam a contagem, o que o corte sozinho não faz. -
max_dim, no própriodiagonalize_fermionic_hamiltonian, limita cada setor de spin de todo subespaço que o laço diagonaliza, inclusive o unido. Ele atua como salvaguarda e se aplica ao que quer que a política retorne, depois que o transporte é unido a novas amostras.
A execução acima define max_strings_per_trim=250 e max_dim=250 juntos, de modo que nem o que cada lote contribui nem o subespaço construído a partir dele possam crescer descontroladamente.
Use um autosolver externo
Você pode usar o trim SQD com um autosolver externo passando o argumento 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,
)
O trecho anterior requer que o pacote sbd-eigensolver esteja instalado a partir do PyPI.
Distribua o trabalho
Com TrimPolicy, o laço chama sci_solver duas vezes por iteração: uma com todos os lotes e outra com o subespaço unido. Ambas as chamadas passam pela interface comum de sci_solver, que o suporte a múltiplos processos deste pacote invoca coletivamente em cada rank MPI.
Dois níveis de paralelismo estão potencialmente disponíveis, e eles são independentes:
-
Dentro de uma diagonalização. Um solver como o SBD pode distribuir uma única diagonalização entre os ranks.
-
Entre os lotes. Um solver poderia, em princípio, diagonalizar os lotes simultaneamente.
Dado um conjunto de lotes, o solver determina como delegar o trabalho aos seus processos de trabalho.