@@ -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