Skip to content

Commit b7fdc58

Browse files
committed
mcscf: align GASSCF density and spin APIs with GASCI
1 parent be9c52a commit b7fdc58

2 files changed

Lines changed: 163 additions & 93 deletions

File tree

‎pyscf/mcscf/gasscf.py‎

Lines changed: 32 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,99 +1050,41 @@ def gas_space_info(self):
10501050
"core": core,
10511051
}
10521052

1053-
def _ci_for_active_property(self, ci=None):
1053+
# Public density/property conventions are shared with GASCI: state=None
1054+
# selects the weighted density on SA objects, while state=i selects root i.
1055+
# Keep the Newton solver adapter and its singleton-CI/plan dispatch intact.
1056+
_has_state_weights = gasci.GASCI._has_state_weights
1057+
_state_weights = gasci.GASCI._state_weights
1058+
_base_fcisolver_method = gasci.GASCI._base_fcisolver_method
1059+
_ci_for_rdm = gasci.GASCI._ci_for_rdm
1060+
_gasdm1s_to_ao = gasci.GASCI._gasdm1s_to_ao
1061+
_spin_square_for_ci = gasci.GASCI._spin_square_for_ci
1062+
1063+
def _select_ci(self, ci=None, state=0):
10541064
ci = self.ci if ci is None else ci
10551065
if ci is None:
10561066
raise ValueError("CI vector is not available")
1057-
if isinstance(ci, (list, tuple)):
1058-
_unsupported("state-averaged GAS density wrappers")
1059-
return ci
1060-
1061-
def make_gasdm1s(self, ci=None, ncas=None, nelecas=None):
1062-
"""Return alpha and beta active-space GAS one-particle DMs."""
1063-
1064-
ncas = self.ncas if ncas is None else ncas
1065-
nelecas = self.nelecas if nelecas is None else nelecas
1066-
return self.fcisolver.make_rdm1s(
1067-
self._ci_for_active_property(ci), ncas, nelecas)
1068-
1069-
def make_gasdm1(self, ci=None, ncas=None, nelecas=None):
1070-
"""Return the spin-summed active-space GAS one-particle DM."""
1071-
1072-
ncas = self.ncas if ncas is None else ncas
1073-
nelecas = self.nelecas if nelecas is None else nelecas
1074-
return self.fcisolver.make_rdm1(
1075-
self._ci_for_active_property(ci), ncas, nelecas)
1076-
1077-
def make_gasdm12s(self, ci=None, ncas=None, nelecas=None):
1078-
"""Return spin-resolved active-space GAS 1- and 2-particle DMs."""
1079-
1080-
ncas = self.ncas if ncas is None else ncas
1081-
nelecas = self.nelecas if nelecas is None else nelecas
1082-
return self.fcisolver.make_rdm12s(
1083-
self._ci_for_active_property(ci), ncas, nelecas)
1084-
1085-
def make_gasdm12(self, ci=None, ncas=None, nelecas=None):
1086-
"""Return spin-summed active-space GAS 1- and 2-particle DMs."""
1087-
1088-
ncas = self.ncas if ncas is None else ncas
1089-
nelecas = self.nelecas if nelecas is None else nelecas
1090-
return self.fcisolver.make_rdm12(
1091-
self._ci_for_active_property(ci), ncas, nelecas)
1092-
1093-
def make_gasdm2(self, ci=None, ncas=None, nelecas=None):
1094-
"""Return the spin-summed active-space GAS two-particle DM."""
1095-
1096-
return self.make_gasdm12(ci, ncas, nelecas)[1]
1097-
1098-
def trans_gasdm1s(self, cibra=None, ciket=None, ncas=None, nelecas=None):
1099-
"""Return alpha and beta active-space GAS transition 1-DMs."""
1100-
1101-
ncas = self.ncas if ncas is None else ncas
1102-
nelecas = self.nelecas if nelecas is None else nelecas
1103-
bra = self._ci_for_active_property(cibra)
1104-
ket = self._ci_for_active_property(ciket)
1105-
return self.fcisolver.trans_rdm1s(bra, ket, ncas, nelecas)
1106-
1107-
def trans_gasdm1(self, cibra=None, ciket=None, ncas=None, nelecas=None):
1108-
"""Return the spin-summed active-space GAS transition 1-DM."""
1109-
1110-
ncas = self.ncas if ncas is None else ncas
1111-
nelecas = self.nelecas if nelecas is None else nelecas
1112-
bra = self._ci_for_active_property(cibra)
1113-
ket = self._ci_for_active_property(ciket)
1114-
return self.fcisolver.trans_rdm1(bra, ket, ncas, nelecas)
1115-
1116-
def trans_gasdm12s(self, cibra=None, ciket=None, ncas=None, nelecas=None):
1117-
"""Return spin-resolved active-space GAS transition 1- and 2-DMs."""
1118-
1119-
ncas = self.ncas if ncas is None else ncas
1120-
nelecas = self.nelecas if nelecas is None else nelecas
1121-
bra = self._ci_for_active_property(cibra)
1122-
ket = self._ci_for_active_property(ciket)
1123-
return self.fcisolver.trans_rdm12s(bra, ket, ncas, nelecas)
1124-
1125-
def trans_gasdm12(self, cibra=None, ciket=None, ncas=None, nelecas=None):
1126-
"""Return spin-summed active-space GAS transition 1- and 2-DMs."""
1127-
1128-
ncas = self.ncas if ncas is None else ncas
1129-
nelecas = self.nelecas if nelecas is None else nelecas
1130-
bra = self._ci_for_active_property(cibra)
1131-
ket = self._ci_for_active_property(ciket)
1132-
return self.fcisolver.trans_rdm12(bra, ket, ncas, nelecas)
1133-
1134-
def trans_gasdm2(self, cibra=None, ciket=None, ncas=None, nelecas=None):
1135-
"""Return the spin-summed active-space GAS transition 2-DM."""
1136-
1137-
return self.trans_gasdm12(cibra, ciket, ncas, nelecas)[1]
1138-
1139-
def spin_square(self, ci=None, ncas=None, nelecas=None):
1140-
"""Return ``(<S^2>, 2S+1)`` for a state-specific GAS CI vector."""
1141-
1142-
ncas = self.ncas if ncas is None else ncas
1143-
nelecas = self.nelecas if nelecas is None else nelecas
1144-
return self.fcisolver.spin_square(
1145-
self._ci_for_active_property(ci), ncas, nelecas)
1067+
return gasci.GASCI._select_ci(self, ci, state)
1068+
1069+
def _spin_square_for_roots(self, roots, ncas, nelecas):
1070+
# Bypass the SA averaging wrapper for each selected root while keeping
1071+
# the Newton-owned RDM plan reuse in _GASFCISolver.spin_square.
1072+
method = self._base_fcisolver_method("spin_square")
1073+
return [method(ci, ncas, nelecas) for ci in roots]
1074+
1075+
make_gasdm1 = gasci.GASCI.make_gasdm1
1076+
make_gasdm1s = gasci.GASCI.make_gasdm1s
1077+
make_gasdm12 = gasci.GASCI.make_gasdm12
1078+
make_gasdm12s = gasci.GASCI.make_gasdm12s
1079+
make_gasdm2 = gasci.GASCI.make_gasdm2
1080+
trans_gasdm1 = gasci.GASCI.trans_gasdm1
1081+
trans_gasdm1s = gasci.GASCI.trans_gasdm1s
1082+
trans_gasdm12 = gasci.GASCI.trans_gasdm12
1083+
trans_gasdm12s = gasci.GASCI.trans_gasdm12s
1084+
trans_gasdm2 = gasci.GASCI.trans_gasdm2
1085+
make_rdm1 = gasci.GASCI.make_rdm1
1086+
make_rdm1s = gasci.GASCI.make_rdm1s
1087+
spin_square = gasci.GASCI.spin_square
11461088

11471089
def get_h1gas(self, mo_coeff=None, ncas=None, ncore=None):
11481090
"""Return the effective one-electron Hamiltonian in the GAS space."""

‎pyscf/mcscf/test/test_gasscf.py‎

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1936,16 +1936,144 @@ def test_gasscf_transition_dm_and_spin_wrappers_match_solver(self):
19361936
numpy.asarray(mc.fcisolver.spin_square(ket, 2, (1, 1))),
19371937
atol=1e-12, rtol=0)
19381938

1939-
def test_gasscf_property_wrappers_require_single_ci_vector(self):
1939+
def test_gasscf_property_wrappers_require_ci_and_select_roots(self):
19401940
mol = gto.M(atom="H 0 0 0; H 0 0 0.75", basis="sto-3g", verbose=0)
19411941
mf = scf.RHF(mol)
19421942
mc = gasscf.GASSCF(
19431943
mf, 2, (1, 1), gas_orbs=(2,), gas_restr=None, ncore=0)
19441944

19451945
with self.assertRaisesRegex(ValueError, "CI vector is not available"):
19461946
mc.make_gasdm1()
1947-
with self.assertRaisesRegex(NotImplementedError, "state-averaged"):
1948-
mc.make_gasdm1([numpy.ones(4), numpy.ones(4)])
1947+
roots = [numpy.array([1., 0., 0., 0.]),
1948+
numpy.array([0., 0., 0., 1.])]
1949+
mc.ci = roots
1950+
for state in (0, 1):
1951+
numpy.testing.assert_allclose(
1952+
mc.make_gasdm1(state=state),
1953+
mc.fcisolver.make_rdm1(roots[state], 2, (1, 1)))
1954+
numpy.testing.assert_allclose(mc.make_gasdm1(), mc.make_gasdm1(state=0))
1955+
sa = mc.state_average((.25, .75))
1956+
with self.assertRaisesRegex(ValueError, "one CI vector per state"):
1957+
sa.make_gasdm1(ci=roots[0])
1958+
with self.assertRaisesRegex(ValueError, "root count"):
1959+
sa.make_gasdm1(ci=roots[:1])
1960+
1961+
def _n2_property_pair(self):
1962+
_, mf, mo = self._n2_regression_fixture()
1963+
mc = self._n2_regression_mc(mf)
1964+
ref = gasci.GASCI(
1965+
mf, mc.ncas, mc.nelecas, ncore=mc.ncore,
1966+
gas_orbs=mc.gas_orbs, gas_restr=mc.gas_restr,
1967+
gas_restr_type=mc.gas_restr_type)
1968+
# Keep the embedded MO order. Orthonormal CI probes isolate property
1969+
# dispatch from eigensolver convergence and give distinct root DMs.
1970+
ndet = mc.gas_space_info()["core"]["ndet"]
1971+
probes = numpy.random.default_rng(211).normal(size=(ndet, 2))
1972+
probes = numpy.linalg.qr(probes)[0]
1973+
roots = [probes[:, i].copy() for i in range(2)]
1974+
for obj in (mc, ref):
1975+
obj.mo_coeff = mo.copy()
1976+
obj.ci = [ci.copy() for ci in roots]
1977+
self.addCleanup(mc.close)
1978+
return mc, ref, roots
1979+
1980+
def _assert_property_close(self, actual, expected):
1981+
if isinstance(expected, (tuple, list)):
1982+
self.assertEqual(len(actual), len(expected))
1983+
for a, e in zip(actual, expected):
1984+
self._assert_property_close(a, e)
1985+
else:
1986+
numpy.testing.assert_allclose(actual, expected, atol=2e-11, rtol=0)
1987+
1988+
def test_n2_property_densities_match_gasci_for_ss_sa_and_zero_weight(self):
1989+
mc, ref, roots = self._n2_property_pair()
1990+
names = ("make_gasdm1", "make_gasdm1s", "make_gasdm12",
1991+
"make_gasdm12s", "make_gasdm2", "make_rdm1", "make_rdm1s")
1992+
overlap = mc._scf.get_ovlp()
1993+
for weights in (None, (.25, .75), (1., 0.)):
1994+
obj = mc if weights is None else mc.state_average(weights)
1995+
reference = ref if weights is None else ref.state_average(weights)
1996+
self.addCleanup(obj.close)
1997+
mo_before = obj.mo_coeff.copy()
1998+
for cache in (True, False):
1999+
obj.close()
2000+
obj.cache_plans = cache
2001+
for name in names:
2002+
for state in (None, 0, 1):
2003+
with self.subTest(weights=weights, cache=cache,
2004+
method=name, state=state):
2005+
actual = getattr(obj, name)(state=state)
2006+
expected = getattr(reference, name)(state=state)
2007+
self._assert_property_close(actual, expected)
2008+
# Independently verify weighted AO density and electron count.
2009+
dm0 = ref.make_rdm1(state=0)
2010+
dm1 = ref.make_rdm1(state=1)
2011+
expected = dm0 if weights is None else (
2012+
weights[0] * dm0 + weights[1] * dm1)
2013+
numpy.testing.assert_allclose(obj.make_rdm1(), expected,
2014+
atol=2e-11, rtol=0)
2015+
self.assertAlmostEqual(
2016+
numpy.einsum("ij,ji->", obj.make_rdm1(), overlap),
2017+
obj.mol.nelectron, places=10)
2018+
self.assertAlmostEqual(numpy.trace(obj.make_gasdm1()),
2019+
sum(obj.nelecas), places=10)
2020+
self._assert_property_close(
2021+
obj.make_gasdm1(None, obj.ncas, obj.nelecas, 1),
2022+
ref.make_gasdm1(state=1))
2023+
numpy.testing.assert_array_equal(obj.mo_coeff, mo_before)
2024+
for actual, original in zip(obj.ci, roots):
2025+
numpy.testing.assert_array_equal(actual, original)
2026+
2027+
def test_n2_transition_properties_select_bra_and_ket(self):
2028+
mc, ref, roots = self._n2_property_pair()
2029+
sa = mc.state_average((.25, .75))
2030+
self.addCleanup(sa.close)
2031+
names = ("trans_gasdm1", "trans_gasdm1s", "trans_gasdm12",
2032+
"trans_gasdm12s", "trans_gasdm2")
2033+
for cache in (True, False):
2034+
sa.close()
2035+
sa.cache_plans = cache
2036+
for name in names:
2037+
for bra, ket in ((0, 1), (1, 0), (1, 1)):
2038+
with self.subTest(cache=cache, method=name, bra=bra, ket=ket):
2039+
expected = getattr(ref, name)(roots[bra], roots[ket])
2040+
self._assert_property_close(
2041+
getattr(sa, name)(bra_state=bra, ket_state=ket),
2042+
expected)
2043+
self._assert_property_close(
2044+
getattr(sa, name)(roots[bra], roots[ket]), expected)
2045+
self._assert_property_close(getattr(sa, name)(),
2046+
getattr(ref, name)(roots[0], roots[1]))
2047+
# Reversing a real bra/ket transposes the transition 1-RDM.
2048+
numpy.testing.assert_allclose(
2049+
sa.trans_gasdm1(bra_state=1, ket_state=0),
2050+
sa.trans_gasdm1().T, atol=2e-11, rtol=0)
2051+
numpy.testing.assert_allclose(
2052+
sa.trans_gasdm1(bra_state=1, ket_state=1),
2053+
sa.make_gasdm1(state=1), atol=2e-11, rtol=0)
2054+
2055+
def test_n2_spin_properties_match_gasci_and_reuse_newton_plan(self):
2056+
mc, ref, _ = self._n2_property_pair()
2057+
root_spins = [ref.spin_square(state=i)[0] for i in (0, 1)]
2058+
for weights in (None, (.25, .75), (1., 0.)):
2059+
obj = mc if weights is None else mc.state_average(weights)
2060+
self.addCleanup(obj.close)
2061+
obj.close()
2062+
obj.cache_plans = True
2063+
with mock.patch.object(obj.fcisolver, "make_rdm_plan",
2064+
wraps=obj.fcisolver.make_rdm_plan) as create:
2065+
for state in (0, 1):
2066+
self._assert_property_close(obj.spin_square(state=state),
2067+
ref.spin_square(state=state))
2068+
ss, multiplicity = obj.spin_square()
2069+
expected = root_spins[0] if weights is None else numpy.dot(
2070+
weights, root_spins)
2071+
self.assertAlmostEqual(ss, expected, places=11)
2072+
self.assertAlmostEqual(multiplicity, numpy.sqrt(4*ss+1), places=11)
2073+
self.assertEqual(create.call_count, 1)
2074+
obj.close()
2075+
obj.cache_plans = False
2076+
self.assertAlmostEqual(obj.spin_square()[0], expected, places=11)
19492077

19502078
def test_newton_solver_hides_native_cas_only_hooks(self):
19512079
base = fci_gas.FCISolver(gas_orbs=(2,))

0 commit comments

Comments
 (0)