Skip to content

Add generalized active space self-consistent field solver - #211

Merged
MatthewRHermes merged 69 commits into
pyscf:masterfrom
yideng2025:feature/newton-gasscf
Sep 19, 2026
Merged

MatthewRHermes merged 69 commits into
pyscf:masterfrom
yideng2025:feature/newton-gasscf

Conversation

@yideng2025

Copy link
Copy Markdown
Contributor

Summary

This PR adds a generalized active space self-consistent field (GASSCF) implementation to pyscf-forge. The implementation builds on the determinant-based GASCI solver and PySCF's existing second-order CASSCF framework newton_casscf. It includes:

  • A GASSCF driver following the existing PySCF CASSCF interface.
  • GAS-aware orbital optimization, including nonredundant inter-GAS active-active rotations.
  • Joint orbital/CI second-order optimization through the native PySCF Newton/CIAH machinery.
  • State-specific and state-averaged GASSCF, including zero-weight state-average roots.
  • GAS-safe core and virtual orbital canonicalization.
  • Energy scanner support through as_scanner().
  • Spin-penalty support inherited from the GASCI solver.
  • Density-fitting support through both density_fit() and DFGASSCF.
  • Unit tests covering GAS-as-CAS limits, genuine restricted-GAS calculations, state averaging, canonicalization, scanners, and density fitting.

Implementation

GASSCF driver

The public interface is provided by:

from pyscf.mcscf import gasscf

and follows the existing PySCF MCSCF object style:

mc = gasscf.GASSCF(
    mf,
    gas_orbs=(2, 4, 2),
    gas_restr=((2, 4), (7, 9), (10, 10)),
    gas_restr_type="cumulative-occ",
    nelecas=(5, 5),
    ncore=2,
)
mc.kernel()

The GAS definition and restriction parsing are shared with pyscf.mcscf.gasci. All GAS restriction formats accepted by GASCI are therefore supported by GASSCF, including explicit spin-supergroups, supergroups, cumulative-occupation restrictions, and RAS-style restrictions.

Orbital optimization

The implementation reuses PySCF's native second-order CASSCF/Newton optimizer rather than introducing a separate orbital-optimization framework. The native augmented-Hessian machinery provides the macro/micro iteration structure, keyframes, trust-region handling, and orbital/CI Newton updates. The GASSCF layer adapts this machinery to the restricted GAS determinant space by providing:

  • GASCI vectors and Hamiltonian contractions.
  • GAS one- and two-electron RDMs and transition RDMs.
  • GAS-aware CI response operations.
  • GAS helper-plan lifetime and cache management.
  • A GAS orbital-rotation mask.

In addition to the usual core-active, core-virtual, and active-virtual rotations, rotations between different GAS subspaces are included because they are nonredundant in GASSCF. Active-active rotations within the same GAS subspace remain redundant and are excluded. For a single GAS containing the complete active space, the implementation reduces to the ordinary CASSCF orbital-optimization problem and is tested against newton_casscf.

State averaging

Ordinary state-averaged GASSCF is supported through the existing PySCF state-average interface. Zero-weight roots are also supported. Such roots are solved and retained in the CI state space and can affect the optimization path, but do not contribute directly to the weighted state-average objective. The state-specific limit is validated using state-average weights of the form

(1.0, 0.0, ...)

and reproduces the corresponding state-specific stationary solution.

Canonicalization and scanners

GASSCF supports canonicalization of the core and virtual orbital spaces. Active space natural orbital rotations are not applied because rotations mixing different GAS subspaces can change the definition of a restricted GAS wave function.

Energy-only potential energy surface scans are supported through:

scanner = mc.as_scanner()

Density fitting

Density fitting is supported through the usual PySCF-style interface:

mc = gasscf.GASSCF(...)
mc = mc.density_fit()

and through the DFGASSCF factory. Both construction paths are covered by the numerical regression tests.

Tests

The implementation is tested at several levels.

GAS-as-CAS limit

A single GAS containing the complete active determinant space is compared with the existing PySCF newton_casscf implementation. This validates the GASSCF adapter and the native Newton driver in the limit where GASSCF reduces to CASSCF.

Genuine restricted-GAS regression

A converged N2 genuine-GAS calculation is used as a numerical regression test:

ncore     = 2
nelecas   = (5, 5)
gas_orbs  = (2, 4, 2)
gas_restr = ((2, 4), (7, 9), (10, 10))

Using converged OpenMolcas GASSCF orbitals, the fixed-orbital GASCI energy is

PySCF      -109.017476499975 Eh
OpenMolcas -109.017476500000 Eh

and the state-specific GASSCF result is

PySCF      -109.017476499980 Eh
OpenMolcas -109.017476500000 Eh

The difference is approximately 2e-11 Eh. The same regression fixture is used to validate:

  • State-specific GASSCF.
  • Zero-weight state averaging.
  • Ordinary state averaging.
  • Density-fitting entry points.
  • Energy scanning.
  • Canonicalization invariance.

The zero-weight state-average result agrees with the state-specific energy to approximately 3e-11 Eh. Core/virtual canonicalization preserves the fixed-orbital GAS energy to approximately 2e-12 Eh.

Unit tests

The current test suites pass with:

pyscf.mcscf.test.test_gasscf: 59 tests passed
pyscf.mcscf.test.test_gasci:  23 tests passed

They can be run with:

python -m unittest pyscf.mcscf.test.test_gasscf
python -m unittest pyscf.mcscf.test.test_gasci

Scope

This PR is intentionally limited to GASSCF orbital optimization based on the existing determinant-based GASCI solver.

It does not add:

  • State-average-mix GASSCF.
  • State-specific excited-state wrappers.
  • Analytic nuclear gradients or nonadiabatic couplings.
  • The legacy two-step CASSCF driver.

Copilot AI lite review requested due to automatic review settings September 14, 2026 06:05

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.

🟡 Changes recommended

A critical scanner reset defect and several moderate correctness, performance, validation, and concurrency issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds a generalized active-space self-consistent-field solver built on GASCI and PySCF’s Newton/CIAH framework.

Changes:

  • Adds GASSCF orbital optimization, state averaging, scanners, canonicalization, and density fitting.
  • Adds reusable GAS contraction and spin-plan support.
  • Adds tests and a restricted-GAS usage example.
File summaries
File Summary
pyscf/mcscf/test/test_gasci.py Adds spin-plan lifetime and validation tests.
pyscf/mcscf/gasscf.py Implements the GASSCF driver. Findings include one critical scanner/DF reset issue, five moderate issues involving ERI reuse, global stderr, input validation, RAS handling, and scanner invariants, plus one documentation nit.
pyscf/mcscf/fci_gas.py Adds reusable GAS spin-contraction planning support.
examples/mcscf/93_gasscf.py Demonstrates restricted-GAS orbital optimization.
Review details

Suppressed comments (5)

pyscf/mcscf/gasscf.py:1152

  • The eris object supplied by the native Newton driver is discarded here, and _run_fixed_orbital_gasci always recomputes the active-space two-electron integrals from the orbitals. This forces a fresh AO-to-MO transformation on every macro iteration (including DF calculations), defeating the precomputed-ERI path and adding a substantial avoidable cost.
        e_tot, e_gas, ci = self._run_fixed_orbital_gasci(
            mo_coeff, ci0, verbose)

pyscf/mcscf/gasscf.py:451

  • This repeats the same eager int conversion for an explicit solver, so a caller can pass a non-integral or Boolean solver.gas_orbs and have it silently changed rather than rejected according to the GASCI input contract. Preserve the original values and validate them before adapting the solver.
    solver.gas_orbs = tuple(int(value) for value in solver.gas_orbs)

pyscf/mcscf/gasscf.py:453

  • Explicit GASCI solvers with RAS restrictions have the same valid zero-sized RAS1/RAS3 cases, but this check rejects them before normalize_gas_spec can remove empty spaces. This makes the explicit-solver construction path inconsistent with GASCI and with the documented RAS input format.
    if any(value <= 0 for value in solver.gas_orbs):
        raise ValueError("explicit GASCI solver gas_orbs entries must be positive")

pyscf/mcscf/gasscf.py:1484

  • The public setter also truncates non-integral and Boolean values, so mutating mc.gas_orbs after construction bypasses the strict GASCI validation even if the constructor is corrected. Route this assignment through the same integer validator instead of converting with int.
        self.fcisolver.gas_orbs = (
            None if value is None else tuple(int(item) for item in value))

pyscf/mcscf/gasscf.py:501

  • extrasym directly changes uniq_var_indices and therefore the orbital variables optimized by a scanner (see lines 937-940), but it is not part of this signature. If a caller changes scanner.extrasym after creating the scanner, __call__ silently accepts a different optimization problem instead of raising like it does for frozen; include the orbital-mask constraint in the scanner invariants.
        "frozen": _digest(mc.frozen),
  • Files reviewed: 4/5 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pyscf/mcscf/gasscf.py
self.mo_coeff, copy=True)
guess_ci = _copy_ci(self.ci if ci0 is None else ci0, signature)

self.reset(mol)

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.

_DFGASSCF inherits PySCF's _DFCASSCF / _DFCAS, and _DFCAS.reset() already calls self.with_df.reset(mol) before forwarding to super().reset(mol). Therefore the auxiliary DF object is already rebound to the new molecule when the scanner calls self.reset(mol). I left the reset implementation unchanged, but added test_df_scanner_reset_updates_with_df_molecule in 3c9a0ef to make this behavior explicit and guard it against regression.

Comment thread pyscf/mcscf/gasscf.py Outdated
if gas_orbs is None:
raise ValueError(
"gas_orbs is required without an explicit GASCI solver")
gas_orbs = tuple(int(value) for value in gas_orbs)

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.

Addressed in 3c9a0ef. GASSCF now uses a shared _validated_user_gas_orbs helper that delegates to the same strict integer validation used by GASCI (addons_gas._integer_vector), instead of eagerly converting entries with int. The constructor, explicit-solver adaptation path, and public gas_orbs setter now all reject floating-point and Boolean values. A regression test covers both (2.5,) and (True,).

Comment thread pyscf/mcscf/gasscf.py Outdated
Comment on lines +407 to +408
if any(value <= 0 for value in gas_orbs):
raise ValueError("gas_orbs entries must be positive integers")

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.

Addressed in 3c9a0ef. I removed the GASSCF-specific requirement that every gas_orbs entry be strictly positive. GASSCF now preserves the user GAS definition and delegates the restriction semantics to the shared GASCI normalizer. This restores support for valid RAS specifications with empty RAS1/RAS3 spaces, such as gas_orbs=(0, 2, 0). The new regression test checks both the normal constructor and explicit-GASCI-solver construction paths and verifies that the kernel space is normalized to (2,).

Comment thread pyscf/mcscf/gasscf.py Outdated
Comment on lines +751 to +754
stderr = sys.stderr
restore_stderr = not isinstance(stderr, _GASSCFLogFilter)
if restore_stderr:
sys.stderr = _GASSCFLogFilter(stderr)

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.

Addressed in 3c9a0ef. GASSCF no longer replaces process-global sys.stderr. The native Newton logging is now handled with a calculation-local _GASSCFLogger together with the existing stream-label filter. The logger suppresses the inherited SO-CASSCF experimental warning without mutating global process state. A regression test also checks that sys.stderr remains unchanged while the warning is filtered.

Comment thread pyscf/mcscf/gasscf.py Outdated
Comment on lines +714 to +717
Notes:
This first implementation stage establishes the public object and
solver ownership convention only. Later commits add the GAS orbital
rotation mask and native CIAH derivative adapter.

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.

Addressed in 3c9a0ef. I updated the public GASSCF docstring and the remaining development-stage comments so they describe the current implementation: the native PySCF Newton/CIAH driver is reused, with GAS-specific CI/RDM/spin operations and the GAS orbital-rotation mask supplied by the adapter. The outdated references to later/staged implementation work have been removed.

@yideng2025

Copy link
Copy Markdown
Contributor Author

Addressed the review findings in 3c9a0ef, including strict GAS input validation, zero-sized RAS spaces, calculation-local logging, native Newton ERIS reuse, scanner orbital-mask invariants, and stale documentation. Added targeted regression coverage; the GASSCF and GASCI test suites pass 64/64 and 23/23 tests respectively.

@yideng2025

Copy link
Copy Markdown
Contributor Author

A short note on why this implementation is based on newton_casscf rather than the more commonly used mc1step CASSCF driver.

During development, we first explored a GASSCF extension of the mc1step/CIAH framework. This works well for ordinary CASSCF because much of the orbital optimization can be described by the fixed-CI orbital Hessian, with the CI wave function updated co-iteratively. For GASSCF, however, rotations between different GAS subspaces are nonredundant and can couple strongly to changes in the restricted CI wave function. In the systems we tested, this CI response was important for the orbital curvature and could not be neglected without degrading convergence.

We therefore tested several mc1step-based variants, including the fixed-CI Hessian and explicit CI-response corrections. The fixed-CI approximation could have difficulty converging for strongly coupled cases, while treating the CI response explicitly introduced a substantially more expensive nested response problem and was difficult to integrate robustly into the CIAH microiterations.

This motivated the final design used in this PR: reuse PySCF's existing newton_casscf framework and solve the orbital and CI variables together in the joint augmented-Hessian/Newton problem. In the GAS-as-CAS limit this reduces to the existing PySCF second-order CASSCF formulation, while for genuine GAS spaces the additional inter-GAS orbital variables and GASCI response are handled within the same joint Newton space.

The intent here is therefore not to replace mc1step as the standard CASSCF optimizer, but to use the existing joint orbital/CI Newton machinery because it provides a more natural and robust treatment of the stronger CI response encountered in GASSCF.

Comment thread pyscf/mcscf/test/test_gasscf.py Outdated
Comment thread pyscf/mcscf/test/test_gasscf.py Outdated
Comment thread pyscf/mcscf/gasscf.py Outdated
@yideng2025

Copy link
Copy Markdown
Contributor Author

I have completed a final pass over the current GASSCF implementation and the associated GASCI interfaces. The implementation now has the following structure:

  • GASSCF reuses PySCF's native newton_casscf joint orbital/CI Newton machinery, with GAS-specific CI contractions, RDM/transition-RDM operations, spin operations, helper-plan management, and the nonredundant inter-GAS orbital rotations supplied by the GAS adapter.

  • The GAS definition and normalization are shared with GASCI, including spin-supergroup, supergroup, cumulative-occ, and ras restrictions.

  • Ordinary state-specific and state-averaged GASSCF are supported, including zero-weight roots.

  • Core/virtual canonicalization is supported, together with optional within-GAS pseudo-natural orbital rotations that transform the CI vectors consistently.

  • Density fitting, energy scanners, and the GASCI-native spin-penalty Hamiltonian are supported.

  • Public e_tot, e_gas, and state energies report the physical energies. When a spin penalty is used, the penalized quantity remains internal to the eigensolver/Newton optimization and is exposed separately through spin_energy_report().

The review feedback has also led to several robustness improvements. In particular, the current code has strict GAS input validation, support for valid zero-sized RAS edge spaces, calculation-local logging without global stream mutation, explicit ownership and lifetime rules for reusable GAS contraction/RDM/spin plans, scanner and DF resource handling, and GAS-preserving CI transformations for pseudo-natural orbital rotations.

The regression coverage now includes the GAS-as-CAS limit, genuine restricted-GAS calculations, state averaging and zero-weight roots, DF and scanner paths, canonicalization invariance, spin-penalty physical/objective energy separation, helper-plan ownership/lifetime, GAS CI orbital transformations against full-FCI references, and finite-difference checks of the orbital gradient including inter-GAS rotations. The OpenMolcas comparison examples are kept separately, while 93_gasscf_h4.py provides a small self-contained introductory GASSCF example.

Unsupported features are explicitly guarded rather than falling through to incompatible CASSCF implementations, including state-average-mix/state-specific excited-state wrappers, spatial symmetry, X2C, solvent models, analytic gradients/NACs, GPU conversion, and the legacy CASSCF optimization drivers.

The commit 2904193 is a documentation/comment cleanup and does not change the numerical implementation.

@MatthewRHermes
MatthewRHermes merged commit f55abdb into pyscf:master Sep 19, 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.

3 participants