Add PySCF FCI solver plugin and pyscf integral-array model - #208
Conversation
- qmp/models/pyscf.py: Model class taking h1e/eri arrays directly (similar to fcidump.py). Includes _restore_eri helper for compressed ERI format. Registers all four network architectures (mlp/u1u1, transformers/u1u1, mlp/u1, transformers/u1). - qmp/plugins/__init__.py: New plugins subpackage init. - qmp/plugins/pyscf.py: PySCF-compatible FCI solver. Solver.kernel(h1, eri, norb, nelec) builds the model, runs a QMP algorithm (bypassing Hydra and sys.exit), then returns (energy, network_state_dict). ci0 warm-starts the network. _PyscfRuntimeContext overrides folder(), setup(), create_model(), create_network() and save() for programmatic use. - pyproject.toml: Updated include to 'qmp*' so all sub-packages are properly discovered on installation. Co-authored-by: hzhangxyz <11623447+hzhangxyz@users.noreply.github.com>
hzhangxyz
left a comment
There was a problem hiding this comment.
修改一下, 输入参数应该是__main__.py里面那个配置文件的内容, 操作方式应该是模仿__main__.py的函数, 只是需要注意, 集成一下现有的 RuntimeContext , 把 folder 重写到一个临时目录, 跑了固定步数后, 从保存的checkpoint文件中读取结果. 通过这个方式, 参数的格式完全复用, 另外, checkpoint中要传给pyscf的数据可以参考haar.py, 其实应该最多只需要config和psi(psi要不要我不知道).
There was a problem hiding this comment.
Pull request overview
Adds a PySCF-compatible “FCI solver” wrapper around QMP algorithms, plus a PySCF integral-array-backed model so QMP can be driven directly from (h1e, eri, norb, nelec) without an FCIDUMP file.
Changes:
- Introduces
qmp.plugins.pyscf.Solverimplementingkernel(h1e, eri, norb, nelec, ci0=...)by running a selected QMP algorithm programmatically. - Adds
qmp.models.pyscf.Modelto build QMP’s internal Hamiltonian from PySCF-style integral arrays (including restoring 4-fold-symmetric compressed ERIs). - Updates packaging discovery to include the new
qmp.pluginssubpackage.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
qmp/plugins/pyscf.py |
Implements PySCF-style solver wrapper and Hydra-bypassing runtime context to run QMP algorithms and return (energy, state_dict) |
qmp/plugins/__init__.py |
Declares plugins subpackage |
qmp/models/pyscf.py |
Adds integral-array model, ERI restore helper, and network configs matching existing FCIDUMP model patterns |
pyproject.toml |
Adjusts setuptools package discovery to include qmp* (so qmp.plugins is installed) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| numpy.ndarray | ||
| Full (norb, norb, norb, norb) ERI array. | ||
| """ | ||
| if eri.ndim == 4: |
There was a problem hiding this comment.
When eri.ndim == 4, _restore_eri() returns the array without validating it matches (norb, norb, norb, norb). If a caller passes a mis-shaped 4D ERI, the failure will happen later (e.g., in permute) with a less clear error. Add an explicit shape check and raise a ValueError with the expected shape.
| if eri.ndim == 4: | |
| if eri.ndim == 4: | |
| expected_shape = (norb, norb, norb, norb) | |
| if eri.shape != expected_shape: | |
| raise ValueError( | |
| f"For 4D ERI, expected shape {expected_shape} for norb={norb}, got {eri.shape}" | |
| ) |
| # Save the caller's gradient state so we can restore it on exit. | ||
| prev_grad_enabled = torch.is_grad_enabled() | ||
|
|
||
| if isinstance(nelec, int): | ||
| nalpha = (nelec + 1) // 2 | ||
| nbeta = nelec - nalpha | ||
| else: | ||
| nalpha, nbeta = int(nelec[0]), int(nelec[1]) | ||
|
|
||
| n_electron = nalpha + nbeta | ||
| n_spin = nalpha - nbeta | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # 2. Build the PySCF model | ||
| # ------------------------------------------------------------------ | ||
| model_config = ModelConfig( | ||
| h1e=numpy.asarray(h1, dtype=numpy.float64), | ||
| eri=numpy.asarray(eri, dtype=numpy.float64), | ||
| n_orbit=norb, | ||
| n_electron=n_electron, | ||
| n_spin=n_spin, | ||
| nuclear_repulsion=nuclear_repulsion, | ||
| ref_energy=ref_energy, | ||
| ) |
There was a problem hiding this comment.
prev_grad_enabled is captured here but the restoration happens only at the end of kernel(). If anything raises before the restore line (e.g., run.main(...)), the caller’s grad mode can be left incorrect. Wrap the main body in try/finally to always restore torch.set_grad_enabled(prev_grad_enabled).
| # Save the caller's gradient state so we can restore it on exit. | |
| prev_grad_enabled = torch.is_grad_enabled() | |
| if isinstance(nelec, int): | |
| nalpha = (nelec + 1) // 2 | |
| nbeta = nelec - nalpha | |
| else: | |
| nalpha, nbeta = int(nelec[0]), int(nelec[1]) | |
| n_electron = nalpha + nbeta | |
| n_spin = nalpha - nbeta | |
| # ------------------------------------------------------------------ | |
| # 2. Build the PySCF model | |
| # ------------------------------------------------------------------ | |
| model_config = ModelConfig( | |
| h1e=numpy.asarray(h1, dtype=numpy.float64), | |
| eri=numpy.asarray(eri, dtype=numpy.float64), | |
| n_orbit=norb, | |
| n_electron=n_electron, | |
| n_spin=n_spin, | |
| nuclear_repulsion=nuclear_repulsion, | |
| ref_energy=ref_energy, | |
| ) | |
| # ------------------------------------------------------------------ | |
| # Save the caller's gradient state so we can restore it on exit. | |
| prev_grad_enabled = torch.is_grad_enabled() | |
| try: | |
| if isinstance(nelec, int): | |
| nalpha = (nelec + 1) // 2 | |
| nbeta = nelec - nalpha | |
| else: | |
| nalpha, nbeta = int(nelec[0]), int(nelec[1]) | |
| n_electron = nalpha + nbeta | |
| n_spin = nalpha - nbeta | |
| # ------------------------------------------------------------------ | |
| # 2. Build the PySCF model | |
| # ------------------------------------------------------------------ | |
| model_config = ModelConfig( | |
| h1e=numpy.asarray(h1, dtype=numpy.float64), | |
| eri=numpy.asarray(eri, dtype=numpy.float64), | |
| n_orbit=norb, | |
| n_electron=n_electron, | |
| n_spin=n_spin, | |
| nuclear_repulsion=nuclear_repulsion, | |
| ref_energy=ref_energy, | |
| ) | |
| finally: | |
| torch.set_grad_enabled(prev_grad_enabled) |
| # them after context._network was first set). | ||
| final_state_dict: dict[str, torch.Tensor] = final_data.get("network", {}) | ||
| if final_state_dict: | ||
| network.load_state_dict(final_state_dict) |
There was a problem hiding this comment.
final_state_dict is taken from final_data.get("network", {}). If the algorithm returns normally (no _AlgorithmComplete) or doesn’t populate final_data, this will be {} and kernel() returns an empty ci even if the network was trained. Add a fallback (e.g., network.state_dict()) when final_state_dict is empty so warm-starting via ci0 remains reliable.
| network.load_state_dict(final_state_dict) | |
| network.load_state_dict(final_state_dict) | |
| else: | |
| # If the algorithm did not provide a final state dict via | |
| # final_data, fall back to the current network parameters so | |
| # that warm-starting via ci0 remains reliable. | |
| final_state_dict = network.state_dict() |
| eri_full = numpy.zeros((norb, norb, norb, norb), dtype=eri.dtype) | ||
| i_idx, j_idx = numpy.tril_indices(norb) | ||
| ij_list = list(zip(i_idx.tolist(), j_idx.tolist())) | ||
| for ij, (i, j) in enumerate(ij_list): | ||
| for kl, (k, m) in enumerate(ij_list): | ||
| val = eri[ij, kl] | ||
| eri_full[i, j, k, m] = val | ||
| eri_full[j, i, k, m] = val | ||
| eri_full[i, j, m, k] = val | ||
| eri_full[j, i, m, k] = val | ||
| eri_full[k, m, i, j] = val | ||
| eri_full[m, k, i, j] = val | ||
| eri_full[k, m, j, i] = val | ||
| eri_full[m, k, j, i] = val |
There was a problem hiding this comment.
_restore_eri() expands the symmetric (nij, nij) ERI via nested Python loops over all nij**2 pair combinations, which can become a bottleneck as norb grows. Consider a vectorized NumPy approach (pair-index maps + advanced indexing) to avoid Python-level O(n^4) looping.
| eri_full = numpy.zeros((norb, norb, norb, norb), dtype=eri.dtype) | |
| i_idx, j_idx = numpy.tril_indices(norb) | |
| ij_list = list(zip(i_idx.tolist(), j_idx.tolist())) | |
| for ij, (i, j) in enumerate(ij_list): | |
| for kl, (k, m) in enumerate(ij_list): | |
| val = eri[ij, kl] | |
| eri_full[i, j, k, m] = val | |
| eri_full[j, i, k, m] = val | |
| eri_full[i, j, m, k] = val | |
| eri_full[j, i, m, k] = val | |
| eri_full[k, m, i, j] = val | |
| eri_full[m, k, i, j] = val | |
| eri_full[k, m, j, i] = val | |
| eri_full[m, k, j, i] = val | |
| # Allocate full 4D ERI tensor | |
| eri_full = numpy.zeros((norb, norb, norb, norb), dtype=eri.dtype) | |
| # Map compressed pair indices (ij) back to orbital indices (i, j) with i >= j | |
| i_idx, j_idx = numpy.tril_indices(norb) | |
| # Build broadcasted index grids for all (ij, kl) combinations | |
| # Shapes after broadcasting: (nij, nij) for each of I, J, K, L | |
| I = i_idx[:, None] | |
| J = j_idx[:, None] | |
| K = i_idx[None, :] | |
| L = j_idx[None, :] | |
| # Assign values for all 8 symmetry-related permutations in a vectorized manner | |
| eri_full[I, J, K, L] = eri | |
| eri_full[J, I, K, L] = eri | |
| eri_full[I, J, L, K] = eri | |
| eri_full[J, I, L, K] = eri | |
| eri_full[K, L, I, J] = eri | |
| eri_full[L, K, I, J] = eri | |
| eri_full[K, L, J, I] = eri | |
| eri_full[L, K, J, I] = eri |
- Solver.__init__ now accepts a single config dict/DictConfig in the same format as __main__.py's YAML config (keys: action, network, optimizer, common). common.max_absolute_step controls when to return. - _PyscfRuntimeContext is now minimal: only overrides folder() and create_model(). setup() and save() are inherited unchanged from RuntimeContext and work via polymorphism (both call self.folder()). - kernel() closely mimics __main__.py: imports modules, builds model, creates context from config.common via dacite, calls setup(), instantiates algorithm, catches SystemExit raised by base save() at max_absolute_step, reads saved checkpoint from tmpdir/data.pth, returns (energy, ci). - ci is the full checkpoint minus random state; for haar it contains ci['haar']['pool'] = (configs, psi). ci0 is saved to tmpdir/data.pth before setup() for warm-starting. - _extract_energy extracts ground-state energy from haar checkpoint: data['haar']['excited'][global-1][0][0]. - Additional cleanups: rename h1 -> h1e in signature, move tmpdir creation inside try block, add .item() guard in _extract_energy, rename global_step -> haar_global_step for clarity. Co-authored-by: hzhangxyz <11623447+hzhangxyz@users.noreply.github.com>
Done in c92e5b9. The plugin has been redesigned as requested:
Example usage: solver = Solver({
"action": {"name": "haar", "params": {}},
"network": {"name": "mlp/u1u1", "params": {}},
"optimizer": {"name": "Adam", "params": {"lr": 1e-3}},
"common": {"device": "cpu", "max_absolute_step": 10},
})
energy, ci = solver.kernel(h1e, eri, norb, nelec) |
hzhangxyz
left a comment
There was a problem hiding this comment.
返回值格式不对, 给个例子:
import numpy as np
from pyscf import fci
import qmp.plugins.pyscf as qmp_pyscf
# -------------------------------
# 1. 定义系统大小
# -------------------------------
norb = 2 # 轨道数
nelec = (1, 1) # alpha/beta 电子数
# -------------------------------
# 2. 构造简单 Hamiltonian
# -------------------------------
# one-body Hamiltonian
h1 = np.array([[1.0, 0.2],
[0.2, 0.8]])
# two-body integrals (chemist notation)
eri = np.zeros((norb, norb, norb, norb))
# 简单填几个值,保证非零交互
eri[0,0,0,0] = 0.7
eri[1,1,1,1] = 0.6
eri[0,1,0,1] = 0.2
eri[0,1,1,0] = 0.2
# -------------------------------
# 3. 初始化 FCI solver
# -------------------------------
solver = fci.direct_spin1.FCI()
mysolver = qmp_pyscf.Solver({
"action": {"name": "haar", "params": {}},
"network": {"name": "mlp/u1", "params": {}},
"optimizer": {"name": "Adam", "params": {"lr": 1e-3}},
"common": {
"device": "cpu",
"dtype": "float64",
"max_absolute_step": 10,
},
})
# -------------------------------
# 4. 求基态
# -------------------------------
energy, ci = solver.kernel(h1, eri, norb, nelec)
print("FCI 能量:", energy)
print("CI wavefunction:", ci)
energy, ci = mysolver.kernel(h1, eri, norb, nelec)
print("FCI 能量:", energy)
#print("CI wavefunction:", ci)
这里参考的ci看起来是两个config的amplitude, 但是现在的ci是一个巨大的dict
Adds a PySCF-compatible FCI solver (
qmp/plugins/pyscf.py) that wraps QMP algorithms behind the standardsolver.kernel(h1e, eri, norb, nelec)interface, plus a supporting model (qmp/models/pyscf.py) that builds a Hamiltonian directly from integral arrays instead of a file.Usage
New files
qmp/models/pyscf.py—Modelbuilt from(h1e, eri, n_orbit, n_electron, n_spin)numpy arrays. Mirrorsfcidump.py: samepermute(0,2,3,1)/2+ spin-orbital expansion, same four network configs (mlp/u1u1,transformers/u1u1,mlp/u1,transformers/u1). Includes_restore_eri()to expand 4-fold symmetric compressed(nij, nij)ERI to full(norb,norb,norb,norb).qmp/plugins/pyscf.py—Solverclass whose__init__accepts a singleconfigdict/DictConfig in the same format as__main__.py's YAML config (action,network,optimizer,common).common.max_absolute_stepcontrols how many global algorithm steps to run before returning. Key internals:_PyscfRuntimeContextsubclassesRuntimeContextand only overridesfolder()(returns a temporary directory) andcreate_model()(injects the pre-built model).setup()andsave()are inherited unchanged and work via normal polymorphism.kernel()closely mimics__main__.py: imports modules, creates the context viadacitefromconfig.common, callssetup(), instantiates and runs the algorithm. Whenmax_absolute_stepis reached, the basesave()writes the checkpoint to the temp dir and callssys.exit(0).kernel()catches the resultingSystemExitand reads the saveddata.pthcheckpoint file to extract the energy and CI data.(energy, ci)where energy is extracted from the checkpoint (forhaar:data["haar"]["excited"][N-1][0][0]) andciis the full checkpoint minus random state. For thehaaralgorithm,ci["haar"]["pool"]contains a(configs, psi)tuple representing the sampled basis and Lanczos-optimised amplitudes.ci0warm-starts the algorithm by saving it totmpdir/data.pthbeforesetup()loads it, exactly mirroring how__main__.pyhandles checkpoint resume.qmp/plugins/__init__.py— package init.Modified files
pyproject.toml—include = ["qmp"]→include = ["qmp*"]so sub-packages includingqmp.pluginsare correctly discovered on install.Original prompt
🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.