Add generalized active space CI solver with OpenMP C kernels - #203
Conversation
Honor FCISolver.spin when normalizing active electron counts and remove GASSCF-only library compatibility paths. Add PySCF source headers and finalize comments in the frozen GAS C kernels.
Cover restriction normalization, GAS-as-CAS behavior, restricted spaces, RDMs, spin guards, multiroot GASCI, and state averaging. Add compact examples for explicit spin-supergroups, restriction formats, and cumulative-occ multiroot analysis.
Use FCISolver.spin consistently when normalizing GAS restrictions, reporting determinant spaces, tracking CI reuse signatures, and evaluating spin-square values. Add a GASCI regression test for spin-derived active electron counts.
There was a problem hiding this comment.
Pull request overview
This PR introduces a determinant-based generalized active space CI (GASCI) capability to pyscf-forge, providing an OpenMP-enabled C backend plus a PySCF-style Python driver and FCISolver bindings, along with tests and usage examples.
Changes:
- Add a GAS-aware
FCISolverimplementation (pyscf.mcscf.fci_gas) backed by new C/OpenMP kernels (determinant space construction, hdiag, contraction, RDMs). - Add a
GASCIdriver (pyscf.mcscf.gasci) following the existing CASCI interface, plus helpers for restriction normalization (pyscf.mcscf.addons_gas). - Add unit tests and examples for restriction formats, solver correctness vs CAS, multiroot RDMs, and state-averaging.
Reviewed changes
Copilot reviewed 12 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pyscf/mcscf/test/test_gasci.py | Adds unit tests for GAS restriction normalization, solver equivalence to CAS in full space, and GASCI workflows. |
| pyscf/mcscf/gasci.py | Introduces the GASCI driver class and scanner support, plus RDM/analysis helpers. |
| pyscf/mcscf/fci_gas.py | Implements a GAS-backed FCISolver interface, including pspace/Davidson logic and spin-penalty support. |
| pyscf/mcscf/addons_gas.py | Provides normalization/validation utilities for GAS restriction formats and state-average wrappers. |
| pyscf/mcscf/_gaslib.py | Adds ctypes bindings and library loading/configuration for libfci_gas. |
| pyscf/lib/mcscf/fci_string_gas.c | Implements GAS determinant/string space construction and link-table generation (OpenMP-enabled). |
| pyscf/lib/mcscf/fci_hdiag_gas.c | Implements GAS Hamiltonian diagonal construction with caching and OpenMP parallelism. |
| pyscf/lib/mcscf/fci_gas.h | Declares the public C API and core GAS data structures for the backend. |
| pyscf/lib/CMakeLists.txt | Adds CMake targets/options to build libfci_gas (including an OpenMP-only build mode). |
| examples/mcscf/90_gasci_spin_supergroup.py | Example: GASCI using explicit spin-supergroup restrictions. |
| examples/mcscf/91_gas_restriction_types.py | Example: normalization equivalence across supported restriction formats. |
| examples/mcscf/92_gasci_multiroot_rdm.py | Example: multiroot GASCI with cumulative restrictions and state/transition RDMs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Might be better to add a CMakeLists.txt under lib/mcscf, like the one of pwscf. |
Move the GAS FCI target into mcscf/CMakeLists.txt while preserving the isolated kernel build. Make binomial initialization thread-safe, select tied diagonal addresses deterministically, and add a regression test.
Thanks for the suggestion. Addressed in 4bb49e3: the GAS FCI target is now defined in pyscf/lib/mcscf/CMakeLists.txt, while the top-level CMake file only handles the build-mode routing and add_subdirectory(mcscf). The Copilot comments were also addressed by making the binomial-table initialization thread-safe and resolving diagonal cutoff ties deterministically with a regression test. Validation completed locally: GCC 13.2.0/CMake 3.26 OpenMP build, 11 unit tests, backend and Python-layer regression tests, 20 × 32-thread initialization stress tests, and examples 90–92 all passed. |
Use a GASCI-specific fixed-orbital kernel wrapper so runtime messages and timers identify the GAS calculation rather than the reused CASCI orchestration. Add a regression test requiring Start GASCI and excluding Start CASCI.
There was a problem hiding this comment.
Would be nice to check the energy value you get here to the values from OpenMolcas for example (where GAS has been implemented previously). At the least, you should also make sure the energy of the GASCI calculation does not change, most of the tests in the file are just checking the RDM elements (and specifically trace of the RDM).
There was a problem hiding this comment.
Would be nice to check the energy value you get here to the values from OpenMolcas for example (where GAS has been implemented previously). At the least, you should also make sure the energy of the GASCI calculation does not change, most of the tests in the file are just checking the RDM elements (and specifically trace of the RDM).
Thank you Matthew. I have addressed both points.
(1) I added fixed-orbital GASCI validation examples against OpenMolcas for six systems: NO+, NO, O2, O2+, O2-, and AlFe2O4+. The PySCF calculations use the same molecular definition and MO coefficients as the corresponding OpenMolcas calculations. I did not include the original OpenMolcas .h5 or .out output files in the repository; instead, I saved the required molecular data and MO coefficients as .npz files under data/, together with the corresponding .xyz geometries.
The PySCF GASCI energies are compared directly with the OpenMolcas results, and all tested roots agree within 1E-8 Eh. This tolerance is consistent with the fact that OpenMolcas prints the total energies to eight digits after the decimal point.
(2) I also added an explicit O2 triplet energy regression test to pyscf/mcscf/test/test_gasci.py. It checks the GASCI ground state energy against the fixed reference value -147.5163002382864 Eh. This should catch future changes in the GASCI energy, rather than only checking quantities such as RDM traces.
The updated GASCI test suite now passes all 13 tests.
There was a problem hiding this comment.
The examples aren't actually run in any CI/CD pipeline, so they can drift undetected. Examples should not be the core validation, but just demonstration of how to use the code. I would prefer at least 1 (lightweight) test that validates against the OpenMolcas result, even if there is slightly looser tolerance (like 1e-6 or so). This should ideally be in addition to the o2 triplet test.
There was a problem hiding this comment.
The examples aren't actually run in any CI/CD pipeline, so they can drift undetected. Examples should not be the core validation, but just demonstration of how to use the code. I would prefer at least 1 (lightweight) test that validates against the OpenMolcas result, even if there is slightly looser tolerance (like 1e-6 or so). This should ideally be in addition to the o2 triplet test.
That makes sense. I moved the core OpenMolcas validation into test_gasci.py as a CI-run regression test. The new test embeds the exact NO+ molecular definition and fixed MO coefficients directly in the test file, checks MO orthonormality, computes the lowest 30 states, filters the 10 singlet roots, and compares all of them against the OpenMolcas reference energies with atol=1e-7.
I also simplified the examples so that they serve as demonstrations rather than the main validation suite. They now contain one O2 multiroot example and one AlFe2O4+ single-root example, with representative output included as comments.
| numpy.testing.assert_allclose( | ||
| mc.e_tot, | ||
| -147.5163002382864, | ||
| atol=1e-9, |
There was a problem hiding this comment.
Should be fine, but down the line might be too tight. Could always loosen to 1e-7 (which is a value used in the mcscf module of pyscf).
There was a problem hiding this comment.
Should be fine, but down the line might be too tight. Could always loosen to 1e-7 (which is a value used in the mcscf module of pyscf).
Agreed. I relaxed the O2 triplet energy regression tolerance from 1e-9 to 1e-7, with rtol=0. The new NO+/OpenMolcas multiroot regression also uses an absolute tolerance of 1e-7.
|
Merge conflicts created by #200, please resolve |
There was a problem hiding this comment.
Some final thoughts, after reviewing and reflecting on my own comments. I sense three overall buckets of concern:
- Do we need X? Doesn't PySCF already have a function for X?
- Let's please do everything possible to guarantee memory safety. My preference is to allocate everything possible in the Python layer, and when I can't do that, I always try to make sure the
allocand thefreecommand for any given thing appear in the same function, and I try to keep them as close to each other as possible. - Some cleanup may be possible in the
fci_gas.pykernelfunction.
To this I will here add:
- Can we get an example input file for a simple RASCI calculation? Say for a 3d transition metal active space with semicore p orbitals in RAS1? Of course it's possible to infer how to do it by studying the present examples very closely but users need their hands held.
I'm willing to be told "no" about most of my comments, but please at least pull master and resolve merge conflicts created by #200.
There was a problem hiding this comment.
OK, sorry, I lied, one more thing before I'm done. There is no specific test here of contract_2e or contract_spin as such. If you had a tool to project CI vectors back and forth between the GAS Hilbert space and the FCI Hilbert space, this would be easy to implement even for complicated GAS setups (i.e., not just the GAS-as-CAS limiting case). The GAS Hilbert space is entirely within the FCI Hilbert space, so you can insert resolutions of the identity as you please,
which implies that if you
- Map a GAS CI vector
x(i.e., packed flat ndarray) into a FCI vectorc(rectangular ndarray with many zeros) - Use the FCI
contract_function:oc = contract_o (c) - Project the operator-vector product
ocback into the GAS space (necessarily discarding some nonzero coefficients corresponding to illegal determinants)
then the outcome should be the same as if you did the contract operation with the GAS function: ox = contract_o (x).
There was a problem hiding this comment.
N.B. gas2fci/fci2gas converter functions for the CI vector would also be very useful for developing compatibility with the CSF solver.
There was a problem hiding this comment.
OK, sorry, I lied, one more thing before I'm done. There is no specific test here of
contract_2eorcontract_spinas such. If you had a tool to project CI vectors back and forth between the GAS Hilbert space and the FCI Hilbert space, this would be easy to implement even for complicated GAS setups (i.e., not just the GAS-as-CAS limiting case). The GAS Hilbert space is entirely within the FCI Hilbert space, so you can insert resolutions of the identity as you please,⟨ I GAS | O ^ | Ψ GAS ⟩ = ∑ J , K ⟨ I GAS | J FCI ⟩ ⟨ J FCI | O ^ | K FCI ⟩ ⟨ K FCI | Ψ GAS ⟩
which implies that if you
- Map a GAS CI vector
x(i.e., packed flat ndarray) into a FCI vectorc(rectangular ndarray with many zeros)- Use the FCI
contract_function:oc = contract_o (c)- Project the operator-vector product
ocback into the GAS space (necessarily discarding some nonzero coefficients corresponding to illegal determinants)then the outcome should be the same as if you did the contract operation with the GAS function:
ox = contract_o (x).
Hi Matt. Added reusable gas2fci / fci2gas converters and operator-level projection tests in 83e2ba7.
The tests compare the GAS contractions against the corresponding full-FCI contractions followed by projection, i.e.
For the spin-complete supergroup and RAS cases, the spin-square tests also verify that the GAS space is invariant under
Thanks for the suggestion — the converters should also be useful for future CSF-solver compatibility.
Addressed in |
For (4), I added a simple transition metal RASCI example in The new example uses sextet Al2FeO4+ with
This gives a RASCI(29e,22o) space with 180,486 determinants. The example is also cross-validated against a fixed-orbital OpenMolcas RASCI calculation. The OpenMolcas and PySCF energies are |
|
I found that the MO ordering in the previous Al2FeO4+ RASCI example was not preserved as intended. This did not affect the numerical OpenMolcas/PySCF consistency check or the correctness of the implementation, but it changed the chemical interpretation of the RAS spaces. I updated I also checked the MO shapes and ordering for the other examples under |
|
I simplified the binomial lookup by replacing the runtime-initialized table with a static read-only table in binom_tab[(n << 5) + k]while removing |
|
I made a small style-only cleanup for the GAS C/Python comments and spacing in |
MatthewRHermes
left a comment
There was a problem hiding this comment.
Looks good to me!
Summary
This PR adds a determinant-based generalized active space configuration interaction (GASCI) implementation to
pyscf-forge.It includes:
FCISolverinterface for the GAS FCI backend.GASCIdriver following the existing PySCF CASCI interface.Implementation
C backend
The new GAS FCI backend constructs the allowed determinant space from legal alpha/beta occupation sectors and organizes determinants into GAS blocks.
The backend provides:
The C backend is built as
libfci_gasthrough the CMake configuration inpyscf/lib.Python interface
The Python layer provides:
pyscf.mcscf.fci_gas.FCISolver.pyscf.mcscf.gasci.GASCI.FCISolver.spinwhen inferring active alpha/beta electron counts.A GAS containing the complete active-space determinant set is tested against the existing PySCF CAS backend.
GAS restriction formats
The interface supports four equivalent or complementary ways to define a GAS space:
spin-supergroup: explicit alpha and beta occupations in every GAS subspace.supergroup: explicit total occupations in every GAS subspace.cumulative-occ: lower and upper bounds on cumulative subspace occupations.ras: RAS1 hole and RAS3 electron restrictions.The restrictions are normalized to the explicit spin-supergroups used by the C backend.
Tests
The following validation was performed:
git diff --check: passed.The formal unit tests can be run with:
python -m unittest discover \ -v \ -s pyscf/mcscf/test \ -p 'test_gasci.py'The included examples demonstrate:
Scope
This PR is intentionally limited to GASCI.
Orbital-optimized GASSCF functionality is not included because it requires additional implementation and independent correctness validation.