Add basic orbital optimization for a fixed 1-/2-RDM (restricted case) - #1442
Add basic orbital optimization for a fixed 1-/2-RDM (restricted case)#1442SemyonAndreyev wants to merge 2 commits into
Conversation
Fixes quantumlib#711. A common post-processing step for a correlated calculation (CASCI/FCI in a truncated active space, or a 1-/2-RDM recovered from a quantum device via sample-based diagonalization) is to ask whether a different choice of one-particle orbitals would lower the total energy, holding the RDM itself fixed. optimize_orbitals() reuses the existing restricted-orbital generator parametrization from hartree_fock.py (rhf_params_to_matrix) and minimizes the resulting energy over the rotation parameters via scipy (numerical gradient -- deliberately "basic", per the issue's own wording). Verified with three tests against exact-diagonalization ground states of H2 (using the pre-shipped fixture data, no new dependency): - energy at the identity rotation reproduces the FCI energy exactly; - for a full (untruncated) active space, no rotation scores below the FCI floor, and the optimizer started away from kappa=0 converges back to it -- kappa=0 is provably the global minimum in that case, since any rotation among all orbitals stays inside the same complete N-electron Fock space full CI already minimizes over; - for an active-space-truncated RDM (the realistic use case), orbital rotation measurably improves the energy relative to the untruncated canonical-orbital starting point. LiH is not included: generating fresh integrals needs PySCF, which does not build natively on this Windows dev environment (no C/C++ toolchain) -- a natural follow-up once run in a Linux/CI environment. check/format-incremental, check/pylint-changed-files, and check/mypy all clean; full hamiltonians/ test suite (161 tests incl. these 4) passes.
There was a problem hiding this comment.
Code Review
This pull request introduces a basic restricted (closed-shell) orbital optimization feature (optimize_orbitals) to find a rotation of one-particle orbitals that minimizes the total energy for a fixed 1- and 2-particle reduced density matrix (RDM). It includes comprehensive unit tests verifying physical correctness and active-space truncation improvements. The review feedback suggests adding robust input validation checks at the entry point of optimize_orbitals, specifically verifying the shapes of the integral and RDM arrays, ensuring n_electrons is even, and validating the size of the initial_guess parameter vector.
| n_orbitals = one_body_integrals.shape[0] | ||
| nocc = n_electrons // 2 | ||
| nvirt = n_orbitals - nocc | ||
| if nocc <= 0 or nvirt <= 0: |
There was a problem hiding this comment.
The function optimize_orbitals lacks validation for the shapes of the input arrays (one_body_integrals, two_body_integrals, one_rdm, two_rdm) and the parity of n_electrons. Since this is a restricted closed-shell orbital optimization, n_electrons must be even, and the dimensions of the RDMs must match the number of orbitals. Adding defensive validation checks at the beginning of the function prevents runtime errors (such as shape mismatches or index errors) and provides clear, actionable error messages to the user.
if one_body_integrals.ndim != 2 or one_body_integrals.shape[0] != one_body_integrals.shape[1]:
raise ValueError("one_body_integrals must be a square 2D array.")
n_orbitals = one_body_integrals.shape[0]
if two_body_integrals.shape != (n_orbitals, n_orbitals, n_orbitals, n_orbitals):
raise ValueError(
f"two_body_integrals must have shape {(n_orbitals,) * 4}, "
f"but got {two_body_integrals.shape}."
)
if one_rdm.shape != (2 * n_orbitals, 2 * n_orbitals):
raise ValueError(
f"one_rdm must have shape {(2 * n_orbitals, 2 * n_orbitals)}, "
f"but got {one_rdm.shape}."
)
if two_rdm.shape != (2 * n_orbitals,) * 4:
raise ValueError(
f"two_rdm must have shape {(2 * n_orbitals,) * 4}, "
f"but got {two_rdm.shape}."
)
if n_electrons % 2 != 0:
raise ValueError(
f"Restricted closed-shell orbital optimization requires an even number of electrons, "
f"but got n_electrons={n_electrons}."
)
nocc = n_electrons // 2
nvirt = n_orbitals - nocc
if nocc <= 0 or nvirt <= 0:References
- Validate and cast input parameters early in the class constructor or entry points, rather than performing complex type validation and casting in downstream utility functions.
| if initial_guess is None: | ||
| init_params = np.zeros(nocc * nvirt) | ||
| else: | ||
| init_params = np.asarray(initial_guess).flatten() |
There was a problem hiding this comment.
If initial_guess is provided, its size is not validated against the expected number of parameters (nocc * nvirt). If a user passes an initial_guess with an incorrect size, it will lead to an IndexError or silent bugs inside rhf_params_to_matrix. Adding a validation check ensures that the size of initial_guess matches the expected parameter count.
| if initial_guess is None: | |
| init_params = np.zeros(nocc * nvirt) | |
| else: | |
| init_params = np.asarray(initial_guess).flatten() | |
| if initial_guess is None: | |
| init_params = np.zeros(nocc * nvirt) | |
| else: | |
| init_params = np.asarray(initial_guess).flatten() | |
| if init_params.size != nocc * nvirt: | |
| raise ValueError( | |
| f"initial_guess has size {init_params.size}, but expected {nocc * nvirt} " | |
| f"(nocc={nocc}, nvirt={nvirt})." | |
| ) |
References
- Validate and cast input parameters early in the class constructor or entry points, rather than performing complex type validation and casting in downstream utility functions.
|
@SemyonAndreyev Thank you for this contribution. The comments by Gemini Code Assist actually seem reasonable. When you get a chance, can you address them? |
Addresses both high-priority comments from the automated review on PR quantumlib#1442: - validate one_body_integrals/two_body_integrals/one_rdm/two_rdm shapes against each other and against n_electrons parity (restricted/closed-shell requires an even electron count) before they reach energy(), where a mismatch would otherwise surface as an opaque broadcast/index error deep inside general_basis_change or rhf_params_to_matrix. - validate initial_guess size against the expected nocc * nvirt parameter count for the same reason. 4 new regression tests, all pre-existing tests still pass.
|
Addressed both high-priority comments from Gemini Code Assist:
4 new regression tests cover the new checks ( |
Summary
Adds
optimize_orbitals()(hamiltonians/orbital_optimization.py) for Issue #711: given a fixed 1-/2-particle RDM (e.g. from a truncated active-space CI/CASCI calculation, or recovered from a quantum device via sample-based diagonalization), finds a restricted (closed-shell) orbital rotation that minimizes the energy those RDM coefficients would give under a different one-particle basis, holding the RDM fixed.Reuses the existing restricted-orbital parametrization already in
hartree_fock.py(rhf_params_to_matrix— an antihermitian generator restricted to occupied/virtual blocks, exponentiated into a unitary) andgeneral_basis_change(already used byHartreeFockFunctional.__init__) to rotate the Hamiltonian integrals into the trial basis at each step.scipy.optimize.minimizedrives the search; the gradient is scipy's numerical one, not analytic — deliberately "basic," per the issue's own wording.Correctness property, verified rather than assumed: for a full (untruncated) active space, the identity rotation is provably the global minimum of this objective — any rotation keeps the trial state inside the same complete N-electron Fock space that full CI already minimizes over exactly, so no rotation can score below the FCI energy. Tested this directly against real FCI diagonalization data (not asserted): an earlier version of this PR held the Hamiltonian fixed and rotated the RDM instead, expecting the energy landscape to be flat/invariant for a full space — that assumption failed against real H2 FCI data, which is what surfaced the correct formulation (rotate the Hamiltonian, keep the RDM fixed) and the correct test criterion (global minimum, not invariance).
Test plan
test_optimize_orbitals_at_identity_reproduces_full_ci_energy— energy at the identity rotation exactly matches the FCI energy (sanity check that the objective is wired correctly)test_full_space_fci_rdm_is_never_beaten_by_any_rotation— for a full-space RDM, explicit nonzero rotations never score below the FCI floor, and the optimizer started away from the identity converges back down to it (not below)test_optimize_orbitals_improves_a_truncated_active_space— the realistic use case: a CASCI-style truncated-active-space RDM is improved (or at worst unchanged) by orbital rotation between active and excluded spacetest_optimize_orbitals_rejects_degenerate_orbital_split— raisesValueErrorwhen there's no occupied/virtual split to rotate overhamiltonians/test suite: 161/161 passing, no regressionscheck/format-incremental,check/pylint-changed-files,check/mypy(pinned tool versions) all cleanScope note
Test fixtures use H2 (sto-3g and 6-31g, both already shipped in the package's test data — no new dependency). Did not generate a LiH fixture: doing so needs PySCF, which requires a C/C++ toolchain not available in my dev environment; scoped the tests to H2 at multiple bond lengths/basis sets instead, which already exercises both the full-space and truncated-active-space code paths.
Fixes #711.