Pular para o conteúdo principal
Este projeto está em fase beta. Encontrou um bug? Abra uma issue no GitHub. Tem uma pergunta ou ideia? Pergunte no Discussions — adoraríamos receber seu feedback!
Como está a qualidade da tradução nesta página?

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:

  1. Rodada de triagem. Divida as bitstrings em lotes. Diagonalize cada lote e mantenha a fração trim_ratio de suas strings CI, por setor de spin, ordenadas por peso.

  2. 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_trim limita as strings que cada lote contribui, por setor, independentemente da razão. É útil com symmetrize_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_batches vezes mais strings, menos o que os lotes tinham em comum, o que na prática não é muito.

  • carryover_threshold, carryover_ratio e max_carryover limitam 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 que StandardPolicy usa, com o mesmo valor padrão; os outros dois limitam a contagem, o que o corte sozinho não faz.

  • max_dim, no próprio diagonalize_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.