Skip to content

Commit 201d0cb

Browse files
committed
mcscf: validate GASSCF orbital gradients by finite differences
1 parent d0dc6ae commit 201d0cb

1 file changed

Lines changed: 85 additions & 0 deletions

File tree

‎pyscf/mcscf/test/test_gasscf.py‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
from unittest import mock
2828

2929
import numpy
30+
import scipy.linalg
3031

32+
from pyscf import ao2mo
3133
from pyscf import gto
3234
from pyscf import scf
3335
from pyscf.tools import molden
@@ -1244,6 +1246,89 @@ def _n2_regression_mc(mf):
12441246
mc.fcisolver.conv_tol = 1e-10
12451247
return mc
12461248

1249+
def _check_n2_orbital_gradient_finite_difference(self, weights=None, use_df=False):
1250+
_, mf, mo = self._n2_regression_fixture()
1251+
mc = self._n2_regression_mc(mf)
1252+
self.addCleanup(mc.close)
1253+
if use_df:
1254+
mc = mc.density_fit()
1255+
self.addCleanup(mc.close)
1256+
if weights is not None:
1257+
mc = mc.state_average(weights)
1258+
self.addCleanup(mc.close)
1259+
ncore, nocc = mc.ncore, mc.ncore + mc.ncas
1260+
mask = mc.uniq_var_indices(mo.shape[1], ncore, mc.ncas, mc.frozen)
1261+
rows, cols = numpy.where(mask)
1262+
sectors = {
1263+
"core-active": (rows < nocc) & (cols < ncore),
1264+
"core-virtual": (rows >= nocc) & (cols < ncore),
1265+
"active-virtual": (rows >= nocc) & (cols >= ncore),
1266+
"inter-GAS": (rows < nocc) & (cols >= ncore),
1267+
}
1268+
rng = numpy.random.default_rng(211)
1269+
directions = {}
1270+
for name, selected in sectors.items():
1271+
self.assertTrue(numpy.any(selected))
1272+
direction = numpy.zeros(rows.size)
1273+
direction[selected] = rng.normal(size=numpy.count_nonzero(selected))
1274+
directions[name] = direction / numpy.linalg.norm(direction)
1275+
1276+
# The embedded orbitals are close to a stationary singlet solution.
1277+
# Apply a small deterministic rotation so the finite differences probe
1278+
# nonzero gradients, rather than cancellation at that stationary point.
1279+
# Keep the fixture's MO columns/GAS assignment; do not sort orbitals.
1280+
displacement = .04 * sum(directions.values())
1281+
mo = mo @ scipy.linalg.expm(mc.unpack_uniq_var(displacement))
1282+
mc.gasci(mo)
1283+
self.assertTrue(mc.converged)
1284+
ci = mc.ci
1285+
dm1, dm2 = mc.make_gasdm12(ci=ci)
1286+
eris = mc.ao2mo(mo)
1287+
gradient = mc.gen_g_hop(mo, ci, eris)[0][:rows.size]
1288+
self.assertGreater(numpy.linalg.norm(gradient), 1e-3)
1289+
1290+
def energy(orbitals):
1291+
# Independent scalar expectation value at fixed CI coefficients.
1292+
# Do not reoptimize CI at +/-h: gen_g_hop supplies the orbital
1293+
# partial derivative of the joint orbital/CI objective.
1294+
# Rebuild integrals at both displacements, including on the DF path.
1295+
h1, ecore = mc.get_h1gas(orbitals)
1296+
h2 = ao2mo.restore(1, mc.get_h2gas(orbitals), mc.ncas)
1297+
return (ecore + numpy.einsum('pq,qp', h1, dm1)
1298+
+ .5 * numpy.einsum('pqrs,pqrs', h2, dm2))
1299+
1300+
self.assertAlmostEqual(energy(mo), float(mc.e_tot), delta=2e-8)
1301+
combined = sum(directions.values())
1302+
directions["combined"] = combined / numpy.linalg.norm(combined)
1303+
for name, direction in directions.items():
1304+
with self.subTest(weights=weights, df=use_df, sector=name):
1305+
analytic = numpy.dot(gradient, direction)
1306+
self.assertGreater(abs(analytic), 1e-6)
1307+
kappa = mc.unpack_uniq_var(direction)
1308+
errors = []
1309+
for step in (2e-4, 1e-4):
1310+
plus = energy(mo @ scipy.linalg.expm(step * kappa))
1311+
minus = energy(mo @ scipy.linalg.expm(-step * kappa))
1312+
numerical = (plus - minus) / (2 * step)
1313+
errors.append(abs(numerical - analytic))
1314+
self.assertLess(errors[0], 8e-7)
1315+
self.assertLess(errors[1], 2e-7)
1316+
# Central differences have O(h^2) truncation error. Allow for
1317+
# roundoff once the absolute error is already below 1e-8.
1318+
self.assertLess(errors[1], .4 * errors[0] + 1e-8)
1319+
1320+
def test_n2_orbital_gradient_finite_difference(self):
1321+
self._check_n2_orbital_gradient_finite_difference()
1322+
1323+
def test_n2_sa_orbital_gradient_finite_difference(self):
1324+
for weights in ((.3, .7), (1., 0.)):
1325+
self._check_n2_orbital_gradient_finite_difference(weights=weights)
1326+
1327+
def test_n2_df_orbital_gradient_finite_difference(self):
1328+
for weights in (None, (.3, .7)):
1329+
self._check_n2_orbital_gradient_finite_difference(
1330+
weights=weights, use_df=True)
1331+
12471332
def test_n2_numerical_regression_ss_and_zero_weight_sa(self):
12481333
_, mf, mo = self._n2_regression_fixture()
12491334
ss = self._n2_regression_mc(mf)

0 commit comments

Comments
 (0)