Skip to content

Add generalized active space CI solver with OpenMP C kernels - #203

Merged
MatthewRHermes merged 28 commits into
pyscf:masterfrom
yideng2025:feature/gasci
Aug 10, 2026
Merged

Add generalized active space CI solver with OpenMP C kernels#203
MatthewRHermes merged 28 commits into
pyscf:masterfrom
yideng2025:feature/gasci

Conversation

@yideng2025

Copy link
Copy Markdown
Contributor

Summary

This PR adds a determinant-based generalized active space configuration interaction (GASCI) implementation to pyscf-forge.

It includes:

  • OpenMP-enabled C kernels for GAS determinant-space construction, Hamiltonian contraction, Hamiltonian diagonal construction, and reduced density matrices.
  • Python bindings and an FCISolver interface for the GAS FCI backend.
  • A GASCI driver following the existing PySCF CASCI interface.
  • Support for explicit spin-supergroup, supergroup, cumulative-occupation, and RAS-style restrictions.
  • Unit tests and examples covering the main GASCI workflows.

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:

  • GAS determinant-string and block construction.
  • Compressed single-excitation link tables.
  • OpenMP-parallel Hamiltonian contraction.
  • Hamiltonian diagonal construction.
  • Spin-resolved one- and two-particle RDMs.
  • Transition RDMs.
  • Reusable contraction and RDM plans.

The C backend is built as libfci_gas through the CMake configuration in pyscf/lib.

Python interface

The Python layer provides:

  • Normalization and validation of GAS restrictions.
  • C-library loading and data-structure management.
  • pyscf.mcscf.fci_gas.FCISolver.
  • pyscf.mcscf.gasci.GASCI.
  • Single-root and multiroot calculations.
  • State-averaged (for future GASSCF).
  • Spin-square evaluation and spin-penalty support.
  • Consistent propagation of FCISolver.spin when inferring active alpha/beta electron counts.
  • State and transition RDM interfaces.
  • Natural-orbital and GAS pseudo-natural-orbital analysis.
  • GAS determinant-space diagnostics.

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:

  • Dedicated GCC 13.2.0 and CMake 3.26 OpenMP build: passed.
  • GAS backend smoke test: passed.
  • Extended Python-layer regression test: passed.
  • Formal GASCI unit tests: 10 tests passed.
  • GASCI examples 90, 91, and 92: passed.
  • Source and whitespace audit with 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:

  • GASCI with explicit spin-supergroups.
  • Equivalent GAS restriction formats.
  • Multiroot GASCI with RDM, transition-RDM, spin, and natural-orbital analysis.

Scope

This PR is intentionally limited to GASCI.

Orbital-optimized GASSCF functionality is not included because it requires additional implementation and independent correctness validation.

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.
Copilot AI review requested due to automatic review settings July 27, 2026 05:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 FCISolver implementation (pyscf.mcscf.fci_gas) backed by new C/OpenMP kernels (determinant space construction, hdiag, contraction, RDMs).
  • Add a GASCI driver (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.

Comment thread pyscf/lib/CMakeLists.txt
Comment thread pyscf/mcscf/fci_gas.py Outdated
Comment thread pyscf/lib/mcscf/fci_string_gas.c Outdated
@jeanwsr

jeanwsr commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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.
@yideng2025

Copy link
Copy Markdown
Contributor Author

Might be better to add a CMakeLists.txt under lib/mcscf, like the one of pwscf.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pyscf/mcscf/test/test_gasci.py Outdated
numpy.testing.assert_allclose(
mc.e_tot,
-147.5163002382864,
atol=1e-9,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pyscf/lib/mcscf/fci_contract_gas.c Outdated
@MatthewRHermes

Copy link
Copy Markdown
Collaborator

Merge conflicts created by #200, please resolve

Comment thread pyscf/lib/mcscf/fci_rdm_gas.c Outdated
Comment thread pyscf/mcscf/test/test_gasci.py
Comment thread pyscf/mcscf/test/test_gasci.py
Comment thread pyscf/mcscf/addons_gas.py Outdated
Comment thread pyscf/mcscf/addons_gas.py
Comment thread pyscf/mcscf/addons_gas.py Outdated
Comment thread pyscf/mcscf/addons_gas.py Outdated
Comment thread pyscf/mcscf/addons_gas.py Outdated
Comment thread pyscf/mcscf/fci_gas.py Outdated
Comment thread pyscf/mcscf/fci_gas.py Outdated
Comment thread pyscf/mcscf/fci_gas.py
Comment thread pyscf/mcscf/fci_gas.py
Comment thread pyscf/mcscf/gasci.py Outdated

@MatthewRHermes MatthewRHermes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some final thoughts, after reviewing and reflecting on my own comments. I sense three overall buckets of concern:

  1. Do we need X? Doesn't PySCF already have a function for X?
  2. 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 alloc and the free command for any given thing appear in the same function, and I try to keep them as close to each other as possible.
  3. Some cleanup may be possible in the fci_gas.py kernel function.

To this I will here add:

  1. 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

$$ \langle I_{\textrm{GAS}}|\hat{O}|\Psi_{\textrm{GAS}}\rangle = \sum_{J,K} \langle I_{\textrm{GAS}}|J_{\textrm{FCI}}\rangle\langle J_{\textrm{FCI}}|\hat{O}|K_{\textrm{FCI}}\rangle\langle K_{\textrm{FCI}}|\Psi_{\textrm{GAS}}\rangle $$

which implies that if you

  1. Map a GAS CI vector x (i.e., packed flat ndarray) into a FCI vector c (rectangular ndarray with many zeros)
  2. Use the FCI contract_ function: oc = contract_o (c)
  3. Project the operator-vector product oc back 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).

@MatthewRHermes MatthewRHermes Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B. gas2fci/fci2gas converter functions for the CI vector would also be very useful for developing compatibility with the CSF solver.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

⟨ I GAS | O ^ | Ψ GAS ⟩ = ∑ J , K ⟨ I GAS | J FCI ⟩ ⟨ J FCI | O ^ | K FCI ⟩ ⟨ K FCI | Ψ GAS ⟩

which implies that if you

  1. Map a GAS CI vector x (i.e., packed flat ndarray) into a FCI vector c (rectangular ndarray with many zeros)
  2. Use the FCI contract_ function: oc = contract_o (c)
  3. Project the operator-vector product oc back 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. $O_{\mathrm{GAS}}x = P O_{\mathrm{FCI}} P^\dagger x$, for explicit spin-supergroup, supergroup, and RAS restrictions.

For the spin-complete supergroup and RAS cases, the spin-square tests also verify that the GAS space is invariant under $\hat{S}^2$, i.e. $\hat{S}^2_{\mathrm{FCI}} P^\dagger x = P^\dagger \hat{S}^2_{\mathrm{GAS}} x$.

Thanks for the suggestion — the converters should also be useful for future CSF-solver compatibility.

@yideng2025

Copy link
Copy Markdown
Contributor Author

Merge conflicts created by #200, please resolve

Addressed in 3b845a4.

@yideng2025

Copy link
Copy Markdown
Contributor Author

Some final thoughts, after reviewing and reflecting on my own comments. I sense three overall buckets of concern:

  1. Do we need X? Doesn't PySCF already have a function for X?
  2. 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 alloc and the free command for any given thing appear in the same function, and I try to keep them as close to each other as possible.
  3. Some cleanup may be possible in the fci_gas.py kernel function.

To this I will here add:

  1. 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.

For (4), I added a simple transition metal RASCI example in f72f9f9.

The new example uses sextet Al2FeO4+ with

  • RAS1 = O 2p (12 orbitals), allowing up to 2 holes
  • RAS2 = Fe 3d (5 orbitals)
  • RAS3 = Fe 4d (5 orbitals), allowing up to 2 particles

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 -2041.069566720000 Eh and -2041.069566719117 Eh, respectively, differing by about 8.8e-10 Eh.

@yideng2025

Copy link
Copy Markdown
Contributor Author

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 examples/mcscf/gasci_openmolcas/data/al2feo4_plus_rasci_mo.npz with the corrected reference orbitals so that the RAS partition now matches the intended assignment: O 2p in RAS1, Fe 3d in RAS2, and Fe 4d in RAS3. With the updated file, the OpenMolcas/PySCF agreement is:

E(OpenMolcas) = -2045.013598850000 Eh
E(PySCF)      = -2045.013598849092 Eh
diff          = 9.081e-10 Eh
<S^2>         = 8.75000000

I also checked the MO shapes and ordering for the other examples under examples/mcscf/gasci_openmolcas/ and did not find any similar issue.

@yideng2025

Copy link
Copy Markdown
Contributor Author

I simplified the binomial lookup by replacing the runtime-initialized table with a static read-only table in 2c540a7. The previous stdatomic.h-based initialization was correct, but unnecessary for a small fixed C(n,k) table. The new version keeps the same branchless fixed-stride lookup,

binom_tab[(n << 5) + k]

while removing stdatomic.h, binom_state, and init_binom(). I rebuilt libfci_gas.so and reran the GASCI tests/examples; test_gasci.py passes all 20 tests.

@yideng2025

Copy link
Copy Markdown
Contributor Author

I made a small style-only cleanup for the GAS C/Python comments and spacing in 9f7520a. This updates the wording in the contraction-plan comments, fixes a few missing or extra blank lines, and normalizes one over-indented block in fci_contract_gas.c. No logic was changed. test_gasci.py passes all 20 tests.

@MatthewRHermes MatthewRHermes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

@MatthewRHermes
MatthewRHermes merged commit 2b96a94 into pyscf:master Aug 10, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants