|
27 | 27 | from unittest import mock |
28 | 28 |
|
29 | 29 | import numpy |
| 30 | +import scipy.linalg |
30 | 31 |
|
| 32 | +from pyscf import ao2mo |
31 | 33 | from pyscf import gto |
32 | 34 | from pyscf import scf |
33 | 35 | from pyscf.tools import molden |
@@ -1244,6 +1246,89 @@ def _n2_regression_mc(mf): |
1244 | 1246 | mc.fcisolver.conv_tol = 1e-10 |
1245 | 1247 | return mc |
1246 | 1248 |
|
| 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 | + |
1247 | 1332 | def test_n2_numerical_regression_ss_and_zero_weight_sa(self): |
1248 | 1333 | _, mf, mo = self._n2_regression_fixture() |
1249 | 1334 | ss = self._n2_regression_mc(mf) |
|
0 commit comments