-
Notifications
You must be signed in to change notification settings - Fork 436
Add basic orbital optimization for a fixed 1-/2-RDM (restricted case) #1442
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,167 @@ | ||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||
| Basic orbital optimization for a fixed 1- and 2-particle reduced density | ||||||||||||||||||||||||||||
| matrix (RDM), restricted (closed-shell) case. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Motivation (Issue #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 -- expressed as a rotation of the | ||||||||||||||||||||||||||||
| orbitals the RDM was computed in -- would lower the total energy, holding the | ||||||||||||||||||||||||||||
| RDM itself fixed. This module reuses the existing restricted-orbital | ||||||||||||||||||||||||||||
| generator parametrization from `hartree_fock.py` (`rhf_params_to_matrix`, | ||||||||||||||||||||||||||||
| an antihermitian kappa matrix restricted to occupied/virtual blocks, | ||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The OpenFermion code base is admittedly inconsistent on the spelling, but let's go with the more standard one:
Suggested change
|
||||||||||||||||||||||||||||
| exponentiated into a unitary) and asks scipy to minimize the resulting | ||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit-picky detail: please make sure to properly capitalize the various package names (SciPy, NumPy, etc.).
Suggested change
|
||||||||||||||||||||||||||||
| energy over the rotation parameters. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| This is deliberately "basic" (per the issue's own wording): the gradient | ||||||||||||||||||||||||||||
| used is scipy's numerical one, not an analytic one. Each trial rotation is | ||||||||||||||||||||||||||||
| scored by rotating the *Hamiltonian* integrals into the trial basis (via | ||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please, for the love of quantum, reign in your LLM's tendency to use emphasis all over the place. Most of the uses of
Suggested change
|
||||||||||||||||||||||||||||
| `general_basis_change`, the same utility `HartreeFockFunctional.__init__` | ||||||||||||||||||||||||||||
| already uses to change basis) and evaluating that rotated Hamiltonian | ||||||||||||||||||||||||||||
| against the *fixed* given RDM -- i.e. holding the CI wavefunction's | ||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Punctuation.
Suggested change
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, please spell out what the abbreviation "CI" stands for. |
||||||||||||||||||||||||||||
| expansion coefficients fixed while asking what energy those same | ||||||||||||||||||||||||||||
| coefficients would give if they described occupations of a different, | ||||||||||||||||||||||||||||
| rotated one-particle basis instead. This is a physically real question | ||||||||||||||||||||||||||||
| with a nontrivial answer, not a change of labels: reusing a wavefunction's | ||||||||||||||||||||||||||||
| coefficients under rotated orbitals is generally a *different* state, and | ||||||||||||||||||||||||||||
| its energy is generally different from (and, importantly, never lower | ||||||||||||||||||||||||||||
| than -- see below) the state the RDM actually came from. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Correctness/scope note verified in the test suite: for a *full* active | ||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the site note is only relevant for code reviewers. The rest of the text stands on its own.
Suggested change
|
||||||||||||||||||||||||||||
| space (all molecular orbitals included, RDM from an untruncated FCI | ||||||||||||||||||||||||||||
| calculation), the identity rotation (kappa=0) 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, and the | ||||||||||||||||||||||||||||
| optimizer started away from kappa=0 must converge back down to it (not | ||||||||||||||||||||||||||||
| below). The routine's actual use case is the *active-space-truncated* | ||||||||||||||||||||||||||||
| case, where the RDM comes from a CI diagonalization over a strict subset | ||||||||||||||||||||||||||||
| of orbitals -- there, orbital rotation between the active and excluded | ||||||||||||||||||||||||||||
| space is not a symmetry of the truncated problem, and rotating orbitals | ||||||||||||||||||||||||||||
| can genuinely recover some of the energy lost to the truncation (this is | ||||||||||||||||||||||||||||
| exactly the orbital-rotation step of CASSCF-style methods). | ||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| from typing import Optional | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import numpy as np | ||||||||||||||||||||||||||||
| import scipy as sp | ||||||||||||||||||||||||||||
| from scipy.optimize import OptimizeResult | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| from openfermion.hamiltonians.hartree_fock import generate_hamiltonian, rhf_params_to_matrix | ||||||||||||||||||||||||||||
| from openfermion.ops.representations import general_basis_change | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def _energy_from_rdms( | ||||||||||||||||||||||||||||
| hamiltonian_one_body: np.ndarray, | ||||||||||||||||||||||||||||
| hamiltonian_two_body: np.ndarray, | ||||||||||||||||||||||||||||
| constant: float, | ||||||||||||||||||||||||||||
| one_rdm: np.ndarray, | ||||||||||||||||||||||||||||
| two_rdm: np.ndarray, | ||||||||||||||||||||||||||||
| ) -> float: | ||||||||||||||||||||||||||||
| """<H> for a fixed Hamiltonian and a fixed (possibly rotated) RDM pair. | ||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of plain
Suggested change
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This and other docstrings that contain special characters need to be made Python r-strings, to avoid problems with the documentation toolchain. |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Uses the same elementwise-sum-product convention as | ||||||||||||||||||||||||||||
| `InteractionRDM.expectation()` (both tensors are assumed to already be | ||||||||||||||||||||||||||||
| expressed in the same orbital-index basis). | ||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||
| energy = constant | ||||||||||||||||||||||||||||
| energy += np.sum(one_rdm * hamiltonian_one_body).real | ||||||||||||||||||||||||||||
| energy += np.sum(two_rdm * hamiltonian_two_body).real | ||||||||||||||||||||||||||||
| return energy | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def optimize_orbitals( | ||||||||||||||||||||||||||||
| one_body_integrals: np.ndarray, | ||||||||||||||||||||||||||||
| two_body_integrals: np.ndarray, | ||||||||||||||||||||||||||||
| one_rdm: np.ndarray, | ||||||||||||||||||||||||||||
| two_rdm: np.ndarray, | ||||||||||||||||||||||||||||
| n_electrons: int, | ||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||
| nuclear_repulsion: float = 0.0, | ||||||||||||||||||||||||||||
| initial_guess: Optional[np.ndarray] = None, | ||||||||||||||||||||||||||||
| method: str = 'BFGS', | ||||||||||||||||||||||||||||
| verbose: bool = True, | ||||||||||||||||||||||||||||
| sp_options: Optional[dict] = None, | ||||||||||||||||||||||||||||
| ) -> OptimizeResult: | ||||||||||||||||||||||||||||
| """Restricted orbital-rotation optimization for a fixed 1-/2-RDM. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Finds the antihermitian generator kappa (parametrized exactly as in | ||||||||||||||||||||||||||||
| `hartree_fock.rhf_params_to_matrix` -- a rotation restricted to | ||||||||||||||||||||||||||||
| occupied-virtual blocks, using `n_electrons // 2` occupied spatial | ||||||||||||||||||||||||||||
| orbitals) that minimizes | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| E(kappa) = sum_pq h_pq(kappa) D_qp + sum_pqrs V_pqrs(kappa) Gamma_qpsr | ||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is enough math here that it's better to use LaTeX/MathJax notation for the formulas. |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| where h(kappa)/V(kappa) are `one_body_integrals`/`two_body_integrals` | ||||||||||||||||||||||||||||
| rotated into the trial orbital basis U(kappa) = expm(kappa), and D/Gamma | ||||||||||||||||||||||||||||
| are the *fixed* given `one_rdm`/`two_rdm` (i.e. the CI wavefunction's | ||||||||||||||||||||||||||||
| expansion coefficients are held fixed while the orbitals they refer to | ||||||||||||||||||||||||||||
| are rotated, not re-solved at every step). | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||
| one_body_integrals: spatial-orbital one-body integrals, shape | ||||||||||||||||||||||||||||
| (n_orbitals, n_orbitals), in the same reference basis the RDMs | ||||||||||||||||||||||||||||
| were computed in. | ||||||||||||||||||||||||||||
| two_body_integrals: spatial-orbital two-body integrals, shape | ||||||||||||||||||||||||||||
| (n_orbitals,) * 4, chemist ordering matching | ||||||||||||||||||||||||||||
| `hartree_fock.generate_hamiltonian`. | ||||||||||||||||||||||||||||
| one_rdm: fixed spin-orbital 1-RDM, <a^dagger_p a_q>, shape | ||||||||||||||||||||||||||||
| (2 * n_orbitals,) * 2, in the same reference basis. | ||||||||||||||||||||||||||||
| two_rdm: fixed spin-orbital 2-RDM, <a^dagger_p a^dagger_q a_r a_s>, | ||||||||||||||||||||||||||||
|
Comment on lines
+109
to
+111
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, LaTeX notation for these expressions. |
||||||||||||||||||||||||||||
| shape (2 * n_orbitals,) * 4, in the same reference basis. | ||||||||||||||||||||||||||||
| n_electrons: total electron count (used only to split occupied vs. | ||||||||||||||||||||||||||||
| virtual spatial orbitals for the restricted parametrization; | ||||||||||||||||||||||||||||
| the RDM's actual trace need not equal this exactly, e.g. for an | ||||||||||||||||||||||||||||
| active-space RDM computed with frozen core orbitals excluded | ||||||||||||||||||||||||||||
| from `one_body_integrals`/`two_body_integrals` -- pass the | ||||||||||||||||||||||||||||
| electron count for *this* integral set). | ||||||||||||||||||||||||||||
| nuclear_repulsion: constant energy offset added to every evaluation. | ||||||||||||||||||||||||||||
| initial_guess: starting kappa parameter vector. Defaults to zero | ||||||||||||||||||||||||||||
| (start from the reference orbitals, i.e. no rotation). | ||||||||||||||||||||||||||||
| method: scipy.optimize.minimize method. Gradient-free by default | ||||||||||||||||||||||||||||
| (numerical differentiation) -- see module docstring. | ||||||||||||||||||||||||||||
| verbose: passed through as scipy's 'disp' option. | ||||||||||||||||||||||||||||
| sp_options: extra options merged into the scipy optimizer options. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||
| scipy.optimize.OptimizeResult. `result.x` is the optimal kappa | ||||||||||||||||||||||||||||
| parameter vector; `result.fun` is the optimized energy. | ||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||
| n_orbitals = one_body_integrals.shape[0] | ||||||||||||||||||||||||||||
| nocc = n_electrons // 2 | ||||||||||||||||||||||||||||
| nvirt = n_orbitals - nocc | ||||||||||||||||||||||||||||
| if nocc <= 0 or nvirt <= 0: | ||||||||||||||||||||||||||||
|
Comment on lines
+136
to
+163
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function 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
|
||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||
| f"optimize_orbitals needs at least one occupied and one virtual " | ||||||||||||||||||||||||||||
| f"spatial orbital (got n_orbitals={n_orbitals}, n_electrons={n_electrons})" | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| occ = list(range(nocc)) | ||||||||||||||||||||||||||||
| virt = list(range(nocc, n_orbitals)) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def energy(params: np.ndarray) -> float: | ||||||||||||||||||||||||||||
| kappa = rhf_params_to_matrix(params, n_orbitals, occ, virt) | ||||||||||||||||||||||||||||
| rotation = sp.linalg.expm(kappa) | ||||||||||||||||||||||||||||
| rotated_obi = general_basis_change(one_body_integrals, rotation, (1, 0), transpose=False) | ||||||||||||||||||||||||||||
| rotated_tbi = general_basis_change( | ||||||||||||||||||||||||||||
| two_body_integrals, rotation, (1, 1, 0, 0), transpose=False | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| hamiltonian = generate_hamiltonian(rotated_obi, rotated_tbi, nuclear_repulsion) | ||||||||||||||||||||||||||||
| return _energy_from_rdms( | ||||||||||||||||||||||||||||
| hamiltonian.one_body_tensor, | ||||||||||||||||||||||||||||
| hamiltonian.two_body_tensor, | ||||||||||||||||||||||||||||
| hamiltonian.constant, | ||||||||||||||||||||||||||||
| one_rdm, | ||||||||||||||||||||||||||||
| two_rdm, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if initial_guess is None: | ||||||||||||||||||||||||||||
| init_params = np.zeros(nocc * nvirt) | ||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||
| init_params = np.asarray(initial_guess).flatten() | ||||||||||||||||||||||||||||
|
Comment on lines
+187
to
+190
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Suggested change
References
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| sp_optimizer_options = {'disp': verbose} | ||||||||||||||||||||||||||||
| if sp_options is not None: | ||||||||||||||||||||||||||||
| sp_optimizer_options.update(sp_options) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return sp.optimize.minimize(energy, init_params, method=method, options=sp_optimizer_options) | ||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| import itertools | ||
|
|
||
| import numpy as np | ||
| import pytest | ||
| import scipy as sp | ||
|
|
||
| from openfermion.chem import MolecularData | ||
| from openfermion.config import DATA_DIRECTORY | ||
| from openfermion.hamiltonians.hartree_fock import generate_hamiltonian, rhf_params_to_matrix | ||
| from openfermion.hamiltonians.orbital_optimization import optimize_orbitals | ||
| from openfermion.linalg import expectation, get_ground_state, get_sparse_operator | ||
| from openfermion.ops.operators import FermionOperator | ||
| from openfermion.ops.representations import general_basis_change | ||
|
|
||
|
|
||
| def _fci_ground_state_rdms(hamiltonian, n_qubits): | ||
| """1-/2-RDM of the exact ground state, computed directly by expectation | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This and the other docstrings in this file don't follow the style. The first line should be a short sentence, followed by a blank line, followed by more text if needed. |
||
| value against the ground-state vector (not via a measured qubit | ||
| operator) -- the same construction `measurements.get_interaction_rdm` | ||
| uses, but starting from `linalg.get_ground_state` instead of a real | ||
| measurement, appropriate for a known-answer test.""" | ||
| sparse_h = get_sparse_operator(hamiltonian, n_qubits=n_qubits) | ||
| energy, state = get_ground_state(sparse_h) | ||
|
|
||
| one_rdm = np.zeros((n_qubits, n_qubits)) | ||
| for p, q in itertools.product(range(n_qubits), repeat=2): | ||
| op = get_sparse_operator(FermionOperator(((p, 1), (q, 0))), n_qubits=n_qubits) | ||
| one_rdm[p, q] = expectation(op, state).real | ||
|
|
||
| two_rdm = np.zeros((n_qubits,) * 4) | ||
| for p, q, r, s in itertools.product(range(n_qubits), repeat=4): | ||
| op = get_sparse_operator( | ||
| FermionOperator(((p, 1), (q, 1), (r, 0), (s, 0))), n_qubits=n_qubits | ||
| ) | ||
| two_rdm[p, q, r, s] = expectation(op, state).real | ||
|
|
||
| return energy, one_rdm, two_rdm | ||
|
|
||
|
|
||
| def _load_h2(bond_length='0.7414', basis_suffix='sto-3g'): | ||
| m = MolecularData(filename=f"{DATA_DIRECTORY}/H2_{basis_suffix}_singlet_{bond_length}.hdf5") | ||
| m.load() | ||
| return m | ||
|
|
||
|
|
||
| def test_optimize_orbitals_at_identity_reproduces_full_ci_energy(): | ||
| """A necessary correctness check: with the RDM taken from a full | ||
| (untruncated) FCI calculation in the reference orbitals, evaluating the | ||
| objective at kappa=0 (the identity rotation) must reproduce the FCI | ||
| energy exactly -- this is just re-checking energy() is wired up to the | ||
| same accounting `InteractionRDM.expectation` uses, nothing about | ||
| optimization yet.""" | ||
| m = _load_h2() | ||
| hamiltonian = m.get_molecular_hamiltonian() | ||
| fci_energy, one_rdm, two_rdm = _fci_ground_state_rdms(hamiltonian, m.n_qubits) | ||
|
|
||
| result = optimize_orbitals( | ||
| m.one_body_integrals, | ||
| m.two_body_integrals, | ||
| one_rdm, | ||
| two_rdm, | ||
| m.n_electrons, | ||
| nuclear_repulsion=m.nuclear_repulsion, | ||
| initial_guess=np.zeros((m.n_electrons // 2) * (m.n_orbitals - m.n_electrons // 2)), | ||
| method='Nelder-Mead', | ||
| verbose=False, | ||
| sp_options={'maxiter': 1}, # don't actually move -- just evaluate near kappa=0 | ||
| ) | ||
| assert np.isclose(result.fun, fci_energy, atol=1e-6) | ||
|
|
||
|
|
||
| def test_full_space_fci_rdm_is_never_beaten_by_any_rotation(): | ||
| """Physical correctness check, not a code-behavior tautology: when the | ||
| RDM comes from a full-space FCI calculation, no orbital rotation can | ||
| produce a state with LOWER energy than the FCI value, because a | ||
| rotation among all M orbitals stays inside the same complete | ||
| N-electron Fock space that full CI already minimizes over exactly. | ||
| kappa=0 must therefore be a global minimum of energy(kappa) -- the | ||
| optimizer, started away from kappa=0, must converge back down to (not | ||
| below) the FCI energy, and any explicit nonzero kappa must score | ||
| >= the FCI energy.""" | ||
| m = _load_h2() | ||
| hamiltonian = m.get_molecular_hamiltonian() | ||
| fci_energy, one_rdm, two_rdm = _fci_ground_state_rdms(hamiltonian, m.n_qubits) | ||
|
|
||
| # explicit nonzero rotations must not beat the FCI floor | ||
| n_orbitals = m.n_orbitals | ||
| nocc = m.n_electrons // 2 | ||
| occ = list(range(nocc)) | ||
| virt = list(range(nocc, n_orbitals)) | ||
| for scale in (0.3, -0.7, 1.2): | ||
| params = np.full(nocc * (n_orbitals - nocc), scale) | ||
| kappa = rhf_params_to_matrix(params, n_orbitals, occ, virt) | ||
| rotation = sp.linalg.expm(kappa) | ||
| rotated_obi = general_basis_change(m.one_body_integrals, rotation, (1, 0), transpose=False) | ||
| rotated_tbi = general_basis_change( | ||
| m.two_body_integrals, rotation, (1, 1, 0, 0), transpose=False | ||
| ) | ||
| rotated_hamiltonian = generate_hamiltonian(rotated_obi, rotated_tbi, m.nuclear_repulsion) | ||
| energy = rotated_hamiltonian.constant | ||
| energy += np.sum(one_rdm * rotated_hamiltonian.one_body_tensor).real | ||
| energy += np.sum(two_rdm * rotated_hamiltonian.two_body_tensor).real | ||
| assert energy >= fci_energy - 1e-8, ( | ||
| f"rotation with params={scale} scored below the FCI floor -- " | ||
| f"got {energy}, floor is {fci_energy}" | ||
| ) | ||
|
|
||
| # the optimizer, started away from kappa=0, must converge back to the floor | ||
| rng = np.random.default_rng(1234) | ||
| init = rng.normal(scale=0.4, size=nocc * (n_orbitals - nocc)) | ||
| result = optimize_orbitals( | ||
| m.one_body_integrals, | ||
| m.two_body_integrals, | ||
| one_rdm, | ||
| two_rdm, | ||
| m.n_electrons, | ||
| nuclear_repulsion=m.nuclear_repulsion, | ||
| initial_guess=init, | ||
| verbose=False, | ||
| ) | ||
| assert np.isclose(result.fun, fci_energy, atol=1e-5) | ||
| assert result.fun >= fci_energy - 1e-6 | ||
|
|
||
|
|
||
| def test_optimize_orbitals_improves_a_truncated_active_space(): | ||
| """The realistic use case: a CASCI-style active-space-truncated RDM | ||
| (computed via canonical/reference orbitals, which are not generally | ||
| CASSCF-optimal) should either be improved by orbital rotation or, at | ||
| worst, left unchanged -- never made worse than the untruncated | ||
| (kappa=0) starting point.""" | ||
| m = _load_h2(bond_length='0.75', basis_suffix='6-31g') | ||
| # 4 spatial orbitals total; restrict the active CI space to the lowest 2 | ||
| # (drop the top 2 virtuals from the CI problem, but keep them in the | ||
| # one-/two-body integral tensors that optimize_orbitals rotates over -- | ||
| # this is exactly the "orbital rotation between active and excluded | ||
| # space is not a symmetry" scenario orbital optimization targets). | ||
| active_indices = [0, 1] | ||
| active_hamiltonian = m.get_molecular_hamiltonian(active_indices=active_indices) | ||
| n_active_qubits = 2 * len(active_indices) | ||
| active_energy, active_one_rdm, active_two_rdm = _fci_ground_state_rdms( | ||
| active_hamiltonian, n_active_qubits | ||
| ) | ||
|
|
||
| # Pad the active-space RDM back out to the full 4-orbital (8 spin-orbital) | ||
| # tensor shape optimize_orbitals expects, with the excluded orbitals' | ||
| # entries left at zero (unoccupied in this trial density). | ||
| n_orbitals = m.n_orbitals | ||
| n_spin_orbitals = 2 * n_orbitals | ||
| n_active_spin = n_active_qubits | ||
| one_rdm = np.zeros((n_spin_orbitals, n_spin_orbitals)) | ||
| one_rdm[:n_active_spin, :n_active_spin] = active_one_rdm | ||
| two_rdm = np.zeros((n_spin_orbitals,) * 4) | ||
| two_rdm[:n_active_spin, :n_active_spin, :n_active_spin, :n_active_spin] = active_two_rdm | ||
|
|
||
| baseline = optimize_orbitals( | ||
| m.one_body_integrals, | ||
| m.two_body_integrals, | ||
| one_rdm, | ||
| two_rdm, | ||
| n_electrons=2 * len(active_indices), | ||
| nuclear_repulsion=m.nuclear_repulsion, | ||
| initial_guess=np.zeros((len(active_indices)) * (n_orbitals - len(active_indices))), | ||
| method='Nelder-Mead', | ||
| verbose=False, | ||
| sp_options={'maxiter': 1}, | ||
| ) | ||
| optimized = optimize_orbitals( | ||
| m.one_body_integrals, | ||
| m.two_body_integrals, | ||
| one_rdm, | ||
| two_rdm, | ||
| n_electrons=2 * len(active_indices), | ||
| nuclear_repulsion=m.nuclear_repulsion, | ||
| verbose=False, | ||
| ) | ||
| assert np.isclose(baseline.fun, active_energy, atol=1e-6) | ||
| assert optimized.fun <= baseline.fun + 1e-8 | ||
|
|
||
|
|
||
| def test_optimize_orbitals_rejects_degenerate_orbital_split(): | ||
| """No occupied or no virtual spatial orbitals -- nothing to rotate.""" | ||
| obi = np.zeros((2, 2)) | ||
| tbi = np.zeros((2, 2, 2, 2)) | ||
| one_rdm = np.zeros((4, 4)) | ||
| two_rdm = np.zeros((4, 4, 4, 4)) | ||
| with pytest.raises(ValueError): | ||
| optimize_orbitals(obi, tbi, one_rdm, two_rdm, n_electrons=4) # all occupied, no virtuals | ||
| with pytest.raises(ValueError): | ||
| optimize_orbitals(obi, tbi, one_rdm, two_rdm, n_electrons=0) # all virtual, no occupied | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's not enough context for many readers to figure out that "issue 711" refers to a GitHub issue. Please use the URL instead, or just omit mention of it altogether, since it's not essential here.