Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MSDFT method #77

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft

Add MSDFT method #77

wants to merge 1 commit into from

Conversation

sunqm
Copy link
Contributor

@sunqm sunqm commented Nov 4, 2024

@baopengbp This implementation basically follows your original version. I'm not clear what are the "sm-t" eigenvalues, so I didn't implement them in this version. Please have a look whether this implementation makes sense.

det_ovlp.append(phase * np.prod(s_a)*np.prod(s_b))

# One-particle asymmetric density matrix. See also pyscf.scf.uhf.make_asym_dm
dm_01a = c1_a.dot(x_a).dot(c2_a.conj().T)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@baopengbp This density matrix is very singular, leading to huge J,K matrices. These quantities are numerically super sensitive to the underlying SCF results. Does this treatment make sense? Should these coupling cases be screened, when singular s_a and s_b are found?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@baopengbp This density matrix is very singular, leading to huge J,K matrices. These quantities are numerically super sensitive to the underlying SCF results. Does this treatment make sense? Should these coupling cases be screened, when singular s_a and s_b are found?

E = |T1'ST2|H12(P12), P12=T1(T2'ST1)^-1T2',(note alpha and beta, see the equations of appendix in J. Am. Chem. SOC. 1990, 112, 4214, and need the generalized slater-condon rule: J. Chem. Phys. 131, 124113, 2009) so if we set singular values to 1e^-11 if they are less than 1e^-11, the 1e energy and 2e energy will be almost not change, and this method is easier than using the generalized slater-condon rule. Then these very small singular value and their vectors should be not screened.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is not about the mathematical background. Here, the implementation may introduce artificial interactions between states. For instance, consider a molecule with point-group symmetry, the ground state and a single-excitation state belong to different irreps. Ideally, the interaction term between these states should be strictly zero. However, this treatment can mix the two states. Moreover, if these irreps are degenerate in energy, even a small off-diagonal term could cause a state with energy significantly lower than the ground state energy.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The coupling error is about 1e^-11 for one electron because all other singular values are about 1.0 if there is only one or two small singular values.
Another method is using the generalized slater-condon rule.
Maybe we should compare these two methods.

Copy link
Collaborator

@MatthewRHermes MatthewRHermes Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleted my comment again because i misread the context again

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK so if you mentally transform everything to the biorthogonal basis and just think about the generalized Slater-Condon rules I think it can be shown that this still works out. Yes, if there are two determinants which differ by symmetry, then the JK matrices themselves will go crazy. But:

  1. If there is only one zero singular value due to symmetry, then the symmetry of the two-electron integrals will ultimately cancel the crazy (AO-basis) JK matrix. (ii'|jj') and (ij'|ji') are only nonzero if j\to j' corresponds to the same symmetry element as i\to i', but this would imply that the jth singular value must also be zero, and the i==j case is canceled by exchange. So the two-electron part of the energy can't contribute, and neither can the one-electron part because it will be zero by symmetry.
  2. If there are two singular values corresponding to the same symmetry change, then the two states do not in fact have different symmetries. The artificial 1e-11 floored singular values cancel between the two factors of the density matrix and the final multiplication by det_ovlp, and all is well.
  3. If there are more than two zero singular values, then neither of the two terms of the Hamiltonian can cancel all of the 1e-11 factors in det_ovlp, and the whole thing is at most ~1e-11, which is close enough to zero in most cases.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A generalized slater-condon rule code was finished. These two methods gave the same result.

Copy link
Collaborator

@MatthewRHermes MatthewRHermes Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the canceling of self-interaction between Coulomb and exchange is possibly numerically problematic in the one-zero case since the difference between E(J) and E(K) is 11 orders of magnitude smaller than E(J) and E(K) themselves, and rather than discarding that small difference it is precisely the quantity we need. (In the two-zero case it's not a problem: 10^22 is actually where we need our point to float to.) You might go partway to the GSC rules by branching on the case of exactly 1 zero singular value. In the branch, you would omit the zero mode from the density matrix (D) and from det_ovlp, but you would make a second pseudo-density matrix (P = u0 v0') from the vectors of the zero mode, without dividing by anything. Then you would compute the energy asymmetrically as

E = det_ovlp * (h.P + vhf[D].P)

Then to go the rest of the way, the branch for exactly two zero singular values would omit both of those modes from det_ovlp, but you would make the density matrix out of just those modes (P = u0 v0' + u1 v1') with no divisors and the energy would be

E = det_ovlp * vhf[P].P / 2

What you can't do is just index down the left-hand sides of lines 83-84, since the zeros are necessary information to make interactions that fail to couple the two determinants vanish properly.

Copy link

@baopengbp baopengbp Nov 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code was already send to Qiming when it was finished. Here is the main part.

def det_ovlp(mo1, mo2, occ1, occ2, ovlp):
if numpy.sum(occ1) !=numpy.sum(occ2):
raise RuntimeError('Electron numbers are not equal. Electronic coupling does not exist.')
c1_a = mo1[0][:, occ1[0]>0]
c1_b = mo1[1][:, occ1[1]>0]
c2_a = mo2[0][:, occ2[0]>0]
c2_b = mo2[1][:, occ2[1]>0]
o_a = numpy.asarray(reduce(numpy.dot, (c1_a.conj().T, ovlp, c2_a)))
o_b = numpy.asarray(reduce(numpy.dot, (c1_b.conj().T, ovlp, c2_b)))
u_a, s_a, vt_a = scipy.linalg.svd(o_a)
u_b, s_b, vt_b = scipy.linalg.svd(o_b)
sv_a = len(s_a[s_a<1e-8])
sv_b = len(s_b[s_b<1e-8])

s_a = numpy.where(abs(s_a) > 1.0e-11, s_a, 1.0e-11)
s_b = numpy.where(abs(s_b) > 1.0e-11, s_b, 1.0e-11) 
OV = numpy.linalg.det(u_a)*numpy.linalg.det(u_b) \
   * numpy.prod(s_a)*numpy.prod(s_b) \
   * numpy.linalg.det(vt_a)*numpy.linalg.det(vt_b) 
x_a = reduce(numpy.dot, (u_a*numpy.reciprocal(s_a), vt_a))
x_b = reduce(numpy.dot, (u_b*numpy.reciprocal(s_b), vt_b))
return OV, numpy.array((x_a, x_b)), sv_a + sv_b

def make_asym_dm_1(mo1, mo2, occ1, occ2, ovlp, x):
if numpy.sum(occ1) !=numpy.sum(occ2):
raise RuntimeError('Electron numbers are not equal. Electronic coupling does not exist.')
mo1_a = mo1[0][:, occ1[0]>0]
mo1_b = mo1[1][:, occ1[1]>0]
mo2_a = mo2[0][:, occ2[0]>0]
mo2_b = mo2[1][:, occ2[1]>0]
o_a = numpy.asarray(reduce(numpy.dot, (mo1_a.conj().T, ovlp, mo2_a)))
o_b = numpy.asarray(reduce(numpy.dot, (mo1_b.conj().T, ovlp, mo2_b)))
u_a, s_a, vt_a = scipy.linalg.svd(o_a)
u_b, s_b, vt_b = scipy.linalg.svd(o_b)
s_ah = s_a[s_a>1e-8]
s_bh = s_b[s_b>1e-8]

OV = numpy.linalg.det(u_a)*numpy.linalg.det(u_b) \
   * numpy.prod(s_ah)*numpy.prod(s_bh) \
   * numpy.linalg.det(vt_a)*numpy.linalg.det(vt_b) 
x_a = reduce(numpy.dot, (u_a[:,s_a<1e-8], vt_a[s_a<1e-8]))
x_b = reduce(numpy.dot, (u_b[:,s_b<1e-8], vt_b[s_b<1e-8]))
dm_a = reduce(numpy.dot, (mo1_a, x_a, mo2_a.conj().T))
dm_b = reduce(numpy.dot, (mo1_b, x_b, mo2_b.conj().T))

x_a = reduce(numpy.dot, (u_a[:,s_a>1e-8], vt_a[s_a>1e-8]))
x_b = reduce(numpy.dot, (u_b[:,s_b>1e-8], vt_b[s_b>1e-8]))
dm_ra = reduce(numpy.dot, (mo1_a, x_a, mo2_a.conj().T))
dm_rb = reduce(numpy.dot, (mo1_b, x_b, mo2_b.conj().T))

return OV, numpy.array((dm_a, dm_b)), numpy.array((dm_ra, dm_rb))

def make_asym_dm_2(mo1, mo2, occ1, occ2, ovlp, x):
if numpy.sum(occ1) !=numpy.sum(occ2):
raise RuntimeError('Electron numbers are not equal. Electronic coupling does not exist.')
mo1_a = mo1[0][:, occ1[0]>0]
mo1_b = mo1[1][:, occ1[1]>0]
mo2_a = mo2[0][:, occ2[0]>0]
mo2_b = mo2[1][:, occ2[1]>0]
o_a = numpy.asarray(reduce(numpy.dot, (mo1_a.conj().T, ovlp, mo2_a)))
o_b = numpy.asarray(reduce(numpy.dot, (mo1_b.conj().T, ovlp, mo2_b)))
u_a, s_a, vt_a = scipy.linalg.svd(o_a)
u_b, s_b, vt_b = scipy.linalg.svd(o_b)
s_ah = s_a[s_a>1e-8]
s_bh = s_b[s_b>1e-8]

OV = numpy.linalg.det(u_a)*numpy.linalg.det(u_b) \
   * numpy.prod(s_ah)*numpy.prod(s_bh) \
   * numpy.linalg.det(vt_a)*numpy.linalg.det(vt_b) 
x_a = reduce(numpy.dot, (u_a[:,s_a<1e-8], vt_a[s_a<1e-8]))
x_b = reduce(numpy.dot, (u_b[:,s_b<1e-8], vt_b[s_b<1e-8]))
dm_a = reduce(numpy.dot, (mo1_a, x_a, mo2_a.conj().T))
dm_b = reduce(numpy.dot, (mo1_b, x_b, mo2_b.conj().T))

return OV, numpy.array((dm_a, dm_b))

def scoup(mol, mo0, mo1, occ0, occ1, sv):
if sv == 'dml':
return scoup_dml(mol, mo0, mo1, occ0, occ1)
else:
return scoup_gsc(mol, mo0, mo1, occ0, occ1)

density matrix limitation

def scoup_dml(mol, mo0, mo1, occ0, occ1):
mf = scf.UHF(mol)

Calculate overlap between two determiant <I|F>

s, x, nsv = det_ovlp(mo0, mo1, occ0, occ1, mf.get_ovlp())

Construct density matrix

dm_01 = mf.make_asym_dm(mo0, mo1, occ0, occ1, x)

One-electron part contrbution

h1e = mf.get_hcore(mol)
e1_01 = numpy.einsum('ji,ji', h1e.conj(), dm_01[0]+dm_01[1])

Two-electron part contrbution. D_{IF} is asymmetric

#vhf_01 = get_veff(mf, dm_01, hermi=0)
vj, vk = mf.get_jk(mol, dm_01, hermi=0)
vhf_01 = vj[0] + vj[1] - vk

New total energy

e_01 = mf.energy_elec(dm_01, h1e, vhf_01)

return s, s * e_01[0], dm_01

Generalized Slater-Condon Rule

def scoup_gsc(mol, mo0, mo1, occ0, occ1):
mf = scf.UHF(mol)

Calculate overlap between two determiant <I|F>

s, x, nsv = det_ovlp(mo0, mo1, occ0, occ1, mf.get_ovlp())
if nsv == 0:
# Construct density matrix
dm_01 = mf.make_asym_dm(mo0, mo1, occ0, occ1, x)
# One-electron part contrbution
h1e = mf.get_hcore(mol)
e1_01 = numpy.einsum('ji,ji', h1e.conj(), dm_01[0]+dm_01[1])
# Two-electron part contrbution. D_{IF} is asymmetric
#vhf_01 = get_veff(mf, dm_01, hermi=0)
vj, vk = mf.get_jk(mol, dm_01, hermi=0)
vhf_01 = vj[0] + vj[1] - vk
# New total energy
e_01 = mf.energy_elec(dm_01, h1e, vhf_01)
coup = s * e_01[0]
elif nsv == 1:
# Construct density matrix
s0, dm_01, dm_r = make_asym_dm_1(mo0, mo1, occ0, occ1, mf.get_ovlp(), x)
# One-electron part contrbution
h1e = mf.get_hcore(mol)
e1_01 = numpy.einsum('ji,ji', h1e.conj(), dm_01[0]+dm_01[1])
# Two-electron part contrbution. D_{IF} is asymmetric
#vhf_01 = get_veff(mf, dm_01, hermi=0)
vj, vk = mf.get_jk(mol, dm_r, hermi=0)
vhf_01 = vj[0] + vj[1] - vk
# New total energy
e_01 = mf.energy_elec(dm_01, h1e, vhf_01)
coup = s0 * e_01[0]
elif nsv == 2:
# Construct density matrix
s0, dm_01 = make_asym_dm_2(mo0, mo1, occ0, occ1, mf.get_ovlp(), x)
# One-electron part contrbution
h1e = mf.get_hcore(mol)
e1_01 = numpy.einsum('ji,ji', h1e.conj(), dm_01[0]+dm_01[1])
# Two-electron part contrbution. D_{IF} is asymmetric
#vhf_01 = get_veff(mf, dm_01, hermi=0)
vj, vk = mf.get_jk(mol, dm_01, hermi=0)
vhf_01 = vj[0] + vj[1] - vk
# New total energy
e_01 = mf.energy_elec(dm_01, h1e, vhf_01)
coup = s0 * e_01[0]
else:
s = coup = dm_01 = 0

return s, coup, dm_01

@baopengbp
Copy link

@baopengbp This implementation basically follows your original version. I'm not clear what are the "sm-t" eigenvalues, so I didn't implement them in this version. Please have a look whether this implementation makes sense.

The "sm-t" use the difference energy difference between mix state and Ms=1 triplet state as the coupling between two symmetry-adapted mix state. This can be more accurate than the approximate HF coupling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants