|
36 | 36 | from pyscf.tools import molden |
37 | 37 | from pyscf.fci import addons as fci_addons |
38 | 38 | from pyscf.fci import direct_spin1 |
| 39 | +from pyscf.fci import spin_op |
39 | 40 | from pyscf.mcscf import addons |
40 | 41 | from pyscf.mcscf import addons_gas |
41 | 42 | from pyscf.mcscf import df as mcdf |
@@ -2920,6 +2921,180 @@ def test_fix_spin_rejects_spin_incomplete_gas_and_invalid_target(self): |
2920 | 2921 | with self.assertRaisesRegex(ValueError, "target S"): |
2921 | 2922 | complete.fix_spin_(shift=.2, ss=.5) |
2922 | 2923 |
|
| 2924 | + def _check_spin_penalty_newton_derivatives(self, use_df=False, weights=None): |
| 2925 | + mol = gto.M(atom='H 0 0 0; H 0 0 .8; H 0 0 1.8; H 0 0 2.6', |
| 2926 | + basis='sto-3g', verbose=0) |
| 2927 | + mf = scf.RHF(mol).run() |
| 2928 | + mc = gasscf.GASSCF( |
| 2929 | + mf, 3, (1, 1), ncore=1, gas_orbs=(1, 2), |
| 2930 | + gas_restr=((0, 1), (2, 2)), gas_restr_type='cumulative-occ') |
| 2931 | + self.addCleanup(mc.close) |
| 2932 | + if use_df: |
| 2933 | + mc = mc.density_fit() |
| 2934 | + self.addCleanup(mc.close) |
| 2935 | + if weights is not None: |
| 2936 | + mc = mc.state_average(weights) |
| 2937 | + self.addCleanup(mc.close) |
| 2938 | + mc.validate_capabilities() |
| 2939 | + mo = mf.mo_coeff |
| 2940 | + eris = mc.ao2mo(mo) |
| 2941 | + h1, core = mc.get_h1gas(mo) |
| 2942 | + h2 = mc.get_h2gas(mo) |
| 2943 | + absorbed = direct_spin1.absorb_h1e(h1, h2, mc.ncas, mc.nelecas, .5) |
| 2944 | + # Independent full-FCI operators projected into a genuine restricted |
| 2945 | + # GAS space. Deliberately mix spin sectors: pure-spin eigenvectors can |
| 2946 | + # hide a missing penalty in the CI gradient and curvature. |
| 2947 | + with mc.fcisolver.make_space(mc.ncas, mc.nelecas) as space: |
| 2948 | + eye = numpy.eye(space.ndet) |
| 2949 | + def project_action(action, column): |
| 2950 | + full = fci_gas.gas2fci(column, space) |
| 2951 | + return fci_gas.fci2gas(action(full), space).ravel() |
| 2952 | + hmat = numpy.column_stack([ |
| 2953 | + project_action(lambda c: direct_spin1.contract_2e( |
| 2954 | + absorbed, c, mc.ncas, mc.nelecas), c) for c in eye]) |
| 2955 | + s2 = numpy.column_stack([ |
| 2956 | + project_action(lambda c: spin_op.contract_ss( |
| 2957 | + c, mc.ncas, mc.nelecas), c) for c in eye]) |
| 2958 | + nroots = mc.fcisolver.nroots |
| 2959 | + rng = numpy.random.default_rng(211) |
| 2960 | + columns = numpy.linalg.qr(rng.normal(size=(eye.shape[0], nroots)))[0] |
| 2961 | + roots = [columns[:, i].copy() for i in range(nroots)] |
| 2962 | + directions = [] |
| 2963 | + for c in roots: |
| 2964 | + v = rng.normal(size=c.size) |
| 2965 | + v -= c.dot(v) * c |
| 2966 | + directions.append(v / numpy.linalg.norm(v)) |
| 2967 | + ci = roots[0] if weights is None else roots |
| 2968 | + w = (1.,) if weights is None else weights |
| 2969 | + native = newton_casscf.gen_g_hop(mc, mo, ci, eris) |
| 2970 | + ngorb = native[0].size - sum(c.size for c in roots) |
| 2971 | + ci_direction = numpy.r_[numpy.zeros(ngorb), numpy.concatenate(directions)] |
| 2972 | + orbital_direction = numpy.r_[rng.normal(size=ngorb), |
| 2973 | + numpy.zeros(native[0].size-ngorb)] |
| 2974 | + u = scipy.linalg.expm(.02 * mc.unpack_uniq_var(orbital_direction[:ngorb])) |
| 2975 | + |
| 2976 | + for target in (0., 2.): # Linear minimum-spin and quadratic penalties. |
| 2977 | + mc.fix_spin_(shift=.2, ss=target) |
| 2978 | + delta = s2 - target * eye |
| 2979 | + penalty = .2 * (delta if target == 0 else delta @ delta) |
| 2980 | + operator = hmat + penalty |
| 2981 | + def energy(angle): |
| 2982 | + vectors = [numpy.cos(angle)*c + numpy.sin(angle)*v |
| 2983 | + for c, v in zip(roots, directions)] |
| 2984 | + return core + sum(a * c.dot(operator @ c) |
| 2985 | + for a, c in zip(w, vectors)) |
| 2986 | + previous = None |
| 2987 | + for cache in (True, False): |
| 2988 | + mc.cache_plans = cache |
| 2989 | + with self.subTest(df=use_df, weights=weights, |
| 2990 | + target=target, cache=cache): |
| 2991 | + g, update, hop, diagonal = mc.gen_g_hop(mo, ci, eris) |
| 2992 | + # Public contraction remains physical, even with fix_spin. |
| 2993 | + numpy.testing.assert_allclose( |
| 2994 | + mc.fcisolver.contract_2e(absorbed, roots[0], |
| 2995 | + mc.ncas, mc.nelecas), |
| 2996 | + hmat @ roots[0], atol=1e-12, rtol=0) |
| 2997 | + first = numpy.dot(g, ci_direction) |
| 2998 | + second = numpy.dot(ci_direction, hop(ci_direction)) |
| 2999 | + self.assertGreater(abs(first-native[0].dot(ci_direction)), 1e-3) |
| 3000 | + for step in (2e-4, 1e-4): |
| 3001 | + plus, minus, center = energy(step), energy(-step), energy(0) |
| 3002 | + self.assertAlmostEqual(first, (plus-minus)/(2*step), delta=2e-7) |
| 3003 | + self.assertAlmostEqual(second, (plus-2*center+minus)/step**2, |
| 3004 | + delta=2e-6) |
| 3005 | + # Compare the full tangent CI response with an independent |
| 3006 | + # projected full-FCI matrix, not just one quadratic form. |
| 3007 | + response = hop(ci_direction)[ngorb:] |
| 3008 | + start = 0 |
| 3009 | + for a, c, v in zip(w, roots, directions): |
| 3010 | + block = response[start:start+c.size] |
| 3011 | + expected = 2*a*(operator @ v - c.dot(operator @ c)*v) |
| 3012 | + numpy.testing.assert_allclose( |
| 3013 | + block-c*c.dot(block), expected-c*c.dot(expected), |
| 3014 | + atol=1e-11, rtol=0) |
| 3015 | + start += c.size |
| 3016 | + # P is orbital independent: no extra orbital or mixed block. |
| 3017 | + numpy.testing.assert_allclose(g[:ngorb], native[0][:ngorb], atol=0, rtol=0) |
| 3018 | + numpy.testing.assert_allclose(hop(orbital_direction), |
| 3019 | + native[2](orbital_direction), atol=1e-12) |
| 3020 | + numpy.testing.assert_allclose(hop(ci_direction)[:ngorb], |
| 3021 | + native[2](ci_direction)[:ngorb], atol=1e-12) |
| 3022 | + # Check the keyframe update at changed, unnormalized CI and |
| 3023 | + # rotated orbitals. Only the normalized CI penalty changes. |
| 3024 | + current = [(1.2+i)*(c+.13*v) |
| 3025 | + for i, (c, v) in enumerate(zip(roots, directions))] |
| 3026 | + current_arg = current[0] if weights is None else current |
| 3027 | + actual = update(u, current_arg) |
| 3028 | + expected = native[1](u, current_arg) |
| 3029 | + start = ngorb |
| 3030 | + for a, c in zip(w, current): |
| 3031 | + c = c / numpy.linalg.norm(c) |
| 3032 | + expected[start:start+c.size] += 2*a*(penalty @ c-c.dot(penalty @ c)*c) |
| 3033 | + start += c.size |
| 3034 | + numpy.testing.assert_allclose(actual, expected, atol=1e-11) |
| 3035 | + # Quadratic diagonal is a documented preconditioner |
| 3036 | + # approximation, shared with Davidson; the HVP is exact. |
| 3037 | + pd = .2 * (s2.diagonal()-target) |
| 3038 | + if target != 0: |
| 3039 | + pd = .2 * (s2.diagonal()-target)**2 |
| 3040 | + expected = native[3].copy() |
| 3041 | + start = ngorb |
| 3042 | + for a, c in zip(w, roots): |
| 3043 | + ep = c.dot(penalty @ c) |
| 3044 | + residual = penalty @ c-ep*c |
| 3045 | + expected[start:start+c.size] += 2*a*(pd-ep-2*residual*c) |
| 3046 | + start += c.size |
| 3047 | + numpy.testing.assert_allclose(diagonal, expected, atol=1e-12) |
| 3048 | + if weights is not None and weights[-1] == 0: |
| 3049 | + numpy.testing.assert_allclose(g[-roots[-1].size:], 0, atol=0) |
| 3050 | + numpy.testing.assert_allclose(hop(ci_direction)[-roots[-1].size:], 0, atol=0) |
| 3051 | + values = (g, hop(ci_direction+orbital_direction), diagonal) |
| 3052 | + if previous is not None: |
| 3053 | + for a, b in zip(values, previous): |
| 3054 | + numpy.testing.assert_allclose(a, b, atol=1e-12) |
| 3055 | + previous = values |
| 3056 | + mc.undo_fix_spin_() |
| 3057 | + |
| 3058 | + def test_spin_penalty_newton_derivatives(self): |
| 3059 | + self._check_spin_penalty_newton_derivatives() |
| 3060 | + |
| 3061 | + def test_spin_penalty_sa_newton_derivatives(self): |
| 3062 | + for weights in ((.3, .7), (1., 0.)): |
| 3063 | + self._check_spin_penalty_newton_derivatives(weights=weights) |
| 3064 | + |
| 3065 | + def test_spin_penalty_df_newton_derivatives(self): |
| 3066 | + for weights in (None, (.3, .7), (1., 0.)): |
| 3067 | + self._check_spin_penalty_newton_derivatives(use_df=True, weights=weights) |
| 3068 | + |
| 3069 | + def test_spin_penalty_kernel_uses_local_newton_response(self): |
| 3070 | + mol = gto.M(atom='H 0 0 0; H 0 0 .8; H 0 0 1.8; H 0 0 2.6', |
| 3071 | + basis='sto-3g', verbose=0) |
| 3072 | + mf = scf.RHF(mol).run() |
| 3073 | + original = tuple(getattr(newton_casscf, name) for name in |
| 3074 | + ('kernel', 'update_orb_ci', 'gen_g_hop')) |
| 3075 | + for weights in (None, (.3, .7), (1., 0.)): |
| 3076 | + mc = gasscf.GASSCF(mf, 3, (1, 1), ncore=1, gas_orbs=(1, 2), |
| 3077 | + gas_restr=((0, 1), (2, 2)), |
| 3078 | + gas_restr_type='cumulative-occ') |
| 3079 | + if weights is not None: |
| 3080 | + mc = mc.state_average(weights) |
| 3081 | + self.addCleanup(mc.close) |
| 3082 | + mc.fix_spin_(shift=.2, ss=0.) |
| 3083 | + mc.max_cycle_macro = mc.max_cycle_micro = 1 |
| 3084 | + seen = [] |
| 3085 | + def callback(env): |
| 3086 | + # Native module functions stay unchanged even while the |
| 3087 | + # calculation is running, not merely restored on return. |
| 3088 | + for name, function in zip(('kernel', 'update_orb_ci', 'gen_g_hop'), original): |
| 3089 | + self.assertIs(getattr(newton_casscf, name), function) |
| 3090 | + self.assertAlmostEqual(env['e_tot'], mc.spin_energy_report()['objective'], 10) |
| 3091 | + seen.append(True) |
| 3092 | + with mock.patch.object(gasscf, 'gen_g_hop', wraps=gasscf.gen_g_hop) as response: |
| 3093 | + result = mc.kernel(callback=callback) |
| 3094 | + self.assertGreater(response.call_count, 0) |
| 3095 | + self.assertTrue(seen) |
| 3096 | + self.assertAlmostEqual(result[0], mc.spin_energy_report()['physical'], 10) |
| 3097 | + |
2923 | 3098 | def _check_physical_energy_result(self, mc, result): |
2924 | 3099 | report = mc.spin_energy_report() |
2925 | 3100 | roots = mc.ci if isinstance(mc.ci, (list, tuple)) else [mc.ci] |
|
0 commit comments