babbush / HistoricalFermiLib

This is repo where we developed FermiLib, which then became OpenFermion
Apache License 2.0
1 stars 0 forks source link

Make MolecularRDM class #20

Closed babbush closed 7 years ago

babbush commented 7 years ago

We need to split MolecularOperator into two classes: one that is specialized for RDMs and the other which is specialized for Hamiltonians. We could have both of these inherit from a common parent which is similar to the current MolecularOperator class.

babbush commented 7 years ago

@Spaceenter: I describe this task some more below. @jarrodmcc: Take a look at what I write here.

The MolecularOperator class is currently a special purpose class for manipulating fermionic operators which come from a molecule. In particular, we use this class to represent molecular Hamiltonians of the form: H = constant + \sum{pq} h{pq} a^\dagger_p aq + 0.5 * \sum{pqrs} h_{pqrs} a^\dagger_p a^\dagger_q a_r as. We could just store this as a FermionOperator but we could also specify it more compactly by just storing the constant, h{pq} and h_{pqrs}. Indeed, this is what the MolecularOperator class does now. In particular, it has the attribute "constant", "one_body_coefficients" and "two_body_coefficients". one_bodycoefficients is a numpy array that is N by N and stores the h{pq} and two_bodycoefficients is a numpy array that is N by N by N by N and stores the h{pqrs} values.

This class supports some convenient things such as myham = MolecularOperator(stuff) h{12} = myham[1,2] h{1234} = my_ham[1,2,3,4]. You can also set elements like this.

A good example of why we need a class completely different from FermionOperator can be seen in the function rotatebasis. Basically, one can rotate the orbital basis of the system with the unitary matrix R = e^{i W} where W = W^\dagger = \sum{pq} w_{pq} a^\dagger_p a_q. So that H' = R^{-1} H R. Naively, one might need N^8 operations to perform this basis rotation. It would be terrible to perform directly on a FermionOperator. But if you supply R to MolecularOperator.rotate_basis(R) it performs the basis transformation in time N^5 using tricks from this website: http://sirius.chem.vt.edu/wiki/doku.php?id=crawdad:programming:project4 By the way, that website has a lot of useful information about this sort of stuff.

Likewise, we have other routines such as the very complicated .jordan_wigner_transform() which are specialized to MolecularOperators for performance. For instance, one can do: fermion_operator = my_operator.get_fermion_operator() qubit_operator = fermion_operator.jordan_wigner_transform() but it is much slower than the direct qubit_operator = my_operator.jordan_wigner_transform(). Another crucial function on the MolecularOperator helps restrict one to an active space. It would also be nice to have a function on MolecularOperator which iterates through just the unique terms (see http://sirius.chem.vt.edu/wiki/doku.php?id=crawdad:programming:project3:hint3.1)

Currently, we are "abusing" the MolecularOperator class and using it for something else in addition to molecular Hamiltonians. We are also using it to store molecular RDM (reduced density matrices): in particular, the 1-RDM and the 2-RDM: D_{pq} = <psi| a^\dagger_p aq |psi> D{pqrs} = <psi| a^\dagger_p a^\dagger_q a_r a_s |psi> Clearly, the RDMs are related to the Hamiltonians and it makes sense for them to be very similar data structures. However, there are some things about these classes that are quite different.

For instance, you can't just apply the Jordan-Wigner transform in the normal way to the RDM. If you call jordan_wigner_transform() on the RDM, you get something that doesn't make sense. Also, you should not try to map a RDM to a fermion operator. So the RDM should not have the method .get_fermion_operator(). But one thing you do want to be able to do is evaluate the expectation value of a QubitTerm / QubitOperator with the molecular RDM. So the molecular RDM should have the basic functionality of the MolecularOperator including all get/set, math and string methods and rotate_basis. There also needs to be a function which gets the RDM in an active space, but we need @jarrodmcc to help with that. The RDM should not have jordan_wigner_transform() on it either. Once the class is made I will also add functionality to the MolecularRDM class.

I want to point out that I'm not 100% happy with MolecularOperators being a class that only works for molecules. We could probably generalize it to something that will work for all fermion systems that have, at most, two-body interactions. But maybe that is not worth the effort. I suggest we have MolecularCoefficients be a fairly general class and then have MolecularRDM and MolecularOperator be classes that inherit from MolecularCoefficients. Does this make sense? Jarrod, do you have any thoughts?

Spaceenter commented 7 years ago

Copy instructions here: "If you have the RDM from a CISD calculation then the expectation value of the Hamiltonian with the CISD RDM should be exactly equal to the cisd energy (which is an attribute of MolecularData that is populated whenever a CISD calculation is run)."

Spaceenter commented 7 years ago

@babbush

Two questions:

  1. The Hamiltonian is a QubitOperator which contains multiple QubitTerm(s), so I need to use "get_qubit_expectations(self, qubit_operator)" to calculate the expectation, but the result of that function is a list, rather than a single number, how could that be compared to CISD energy which is a single number?

  2. It seems like we need an additional function like "get_molecular_operator_expectations()", similar to "get_qubit_expectations()", because currently my compute flow is like:

    • Run psi4, get the 'molecule';
    • rdm = molecule. get_molecular_rdm()
    • ham_molecular_operator = molecule. get_molecular_hamiltonian()
    • fermion_operator = ham_molecular_operator.get_fermion_operator()
    • qubit_operator = fermion_operator.jordan_wigner_transform()
    • (finally) expectations = rdm. get_qubit_expectations(qubit_operator)

WDYT?

babbush commented 7 years ago

Hi Wei,

Question 1: Sum the values in the list to get the CISD energy.

Question 2: Yes, I agree. It is very easy. Do an element-wise (direct) multiplication of the RDM with the MolecularOperator. That is, multiply the two_electron_coefficients of both classes and the one_electron_coefficeints of both classes. Then, add together all of the elements of the product. Then, add the molecular operator constant since the RDM constant should always be 1.

Let me know if this makes sense. Hopefully this will also help you to understand just what these RDMs are.

Cheers, Ryan

Spaceenter commented 7 years ago

@babbush

I tried this. Using your method in answer for Question 2, I'm able to get the same expectation value as CISD energy.

However, using your method in answer for Question 1, the answer is different. I copy the test code below - do you see anything wrong? Anyways, I'm going to implement the 2nd method, as that's more straightforward.

===BEGIN===

def setUp(self): geometry = [('H', (0, 0, 0)), ('H', (0, 0, 0.2))] molecule = MolecularData(geometry, 'sto-3g', 1, autosave=False) molecule = run_psi4(molecule, run_scf=False, run_mp2=False, run_cisd=True, run_ccsd=False, run_fci=False, delete_input=True, delete_output=True) self.cisd_energy = molecule.cisd_energy self.rdm = molecule.get_molecular_rdm() self.hamiltonion = molecule.get_molecular_hamiltonian()

def test_get_qubit_expectations(self): fermion_operator = self.hamiltonion.get_fermion_operator() qubit_operator = fermion_operator.jordan_wigner_transform() qubit_expectations = self.rdm.get_qubit_expectations(qubit_operator) got_energy = 0 for qubit_term in qubit_expectations: got_energy += qubit_term.coefficient print self.cisd_energy print print got_energy

===END===

babbush commented 7 years ago

Hi Wei,

Just to make sure that you are loading the calculation at the right bond lengths you should set run_scf to True since the SCF calculation is how the orbitals which define the Hamiltonian are obtained. I just want to make sure that its not autoloading an older calculation at a different bond lengths. Also, and this is a small point, you can write: qubit_operator = self.hamiltonian.jordan_wigner_transform() instead of fermion_operator = self.hamiltonion.get_fermion_operator() qubit_operator = fermion_operator.jordan_wigner_transform() but whatever.

I think I know the problem. rdm.get_qubit_expectations essentially does not pay attention to the coefficients in the Hamiltonian so inside your for loop trying doing this:

term_coefficient = qubit_operator[qubit_term] got_energy += term_coefficient * qubit_term.coefficient

Tell me if that then gives you the correct energy.

Spaceenter commented 7 years ago

Thanks - that works well, see the latest pull request. Sorry I overlooked that.

Spaceenter commented 7 years ago

After the latest pending pull request, this issue could be closed, right? Do you see other thing left for this issue? @babbush

babbush commented 7 years ago

Thanks for all the help Wei. I am about to accept your pull request. I'm going to make some small changes myself today and add a few methods which estimate the variance of sampling an RDM. I'll close the comment soon once I'm done with that.

babbush commented 7 years ago

I'm actually going to make some rather significant changes and submit a pull request so you can see what's going on.