Skip to content

Add from_fermion_operator method to MoelcularHamiltonian class #386

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions python/ffsim/hamiltonians/molecular_hamiltonian.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

from __future__ import annotations

from typing import Dict, Tuple

import dataclasses
import itertools

Expand Down Expand Up @@ -156,6 +158,88 @@ def _fermion_operator_(self) -> FermionOperator:
}
)
return op


@staticmethod
def from_fermion_operator(op: FermionOperator) -> MolecularHamiltonian:
"""Convert a FermionOperator to a MolecularHamiltonian."""
# extract number of spatial orbitals
norb = 1 + max(orb for term in op for _, _, orb in term)

# initialize constant, one‑ and two‑body tensors
constant: float = 0.0
one_body_tensor = np.zeros((norb, norb), dtype=complex)
two_body_tensor = np.zeros((norb, norb, norb, norb), dtype=complex)

# track which (p,q,r,s) we've already processed
seen_2b = set()
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like this wasn't actually used.


for term, coeff in op.items():
# constant term: empty tuple
if len(term) == 0:
if coeff.imag:
raise ValueError(f"Constant term must be real. Instead, got {coeff}.")
constant = coeff.real


# one‑body term: a†_σ,p a_σ,q (σ = α or β)
elif len(term) == 2:
(_, _, p), (_, _, q) = term
valid_1b = [(cre_a(p), des_a(q)), (cre_b(p), des_b(q))]
if term in valid_1b:
one_body_tensor[p, q] += 0.5 * coeff
else:
raise ValueError(
"FermionOperator cannot be converted to "
f"MolecularHamiltonian. The one-body term {term} is not "
"of the form a^\\dagger_{\\sigma, p} a_{\\sigma, q}."
)

# two‑body term: a†_σ,p a†_τ,r a_τ,s a_σ,q
elif len(term) == 4:
(_, _, p), (_, _, r), (_, _, s), (_, _, q) = term

valid_2b = [
(cre_a(p), cre_a(r), des_a(s), des_a(q)),
(cre_a(p), cre_b(r), des_b(s), des_a(q)),
(cre_b(p), cre_a(r), des_a(s), des_b(q)),
(cre_b(p), cre_b(r), des_b(s), des_b(q)),
]
if term not in valid_2b:
raise ValueError(
"FermionOperator cannot be converted to "
f"MolecularHamiltonian. The two-body term {term} is not "
"of the form a^{\\dagger}_{\\sigma, p} a^{\\dagger}_{\\sigma, r} a_{\\sigma, s} a_{\\sigma, q}, or "
"a^{\\dagger}_{\\sigma, p} a^{\\dagger}_{\\tau, r} a_{\\tau, s} a_{\\sigma, q}."
)

key = (p, q, r, s)
if key not in seen_2b:

h_pqrs = 2.0 * coeff
# fill (p,q,r,s) and its 7 symmetric equivalents
Copy link
Collaborator

Choose a reason for hiding this comment

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

In general, there is only four-fold symmetry. See

def test_random_two_body_tensor_symmetry_real():
"""Test random real two-body tensor symmetry."""
n_orbitals = 5
two_body_tensor = ffsim.random.random_two_body_tensor(
n_orbitals, seed=rng, dtype=float
)
assert np.issubdtype(two_body_tensor.dtype, np.floating)
for i, j, k, ell in itertools.product(range(n_orbitals), repeat=4):
val = two_body_tensor[i, j, k, ell]
np.testing.assert_allclose(two_body_tensor[k, ell, i, j], val)
np.testing.assert_allclose(two_body_tensor[j, i, ell, k], val.conjugate())
np.testing.assert_allclose(two_body_tensor[ell, k, j, i], val.conjugate())
np.testing.assert_allclose(two_body_tensor[j, i, k, ell], val)
np.testing.assert_allclose(two_body_tensor[ell, k, i, j], val)
np.testing.assert_allclose(two_body_tensor[i, j, ell, k], val)
np.testing.assert_allclose(two_body_tensor[k, ell, j, i], val)
def test_random_two_body_tensor_symmetry():
"""Test random two-body tensor symmetry."""
n_orbitals = 5
two_body_tensor = ffsim.random.random_two_body_tensor(n_orbitals, seed=rng)
for i, j, k, ell in itertools.product(range(n_orbitals), repeat=4):
val = two_body_tensor[i, j, k, ell]
np.testing.assert_allclose(two_body_tensor[k, ell, i, j], val)
np.testing.assert_allclose(two_body_tensor[j, i, ell, k], val.conjugate())
np.testing.assert_allclose(two_body_tensor[ell, k, j, i], val.conjugate())
.

Actually, I don't think we should try to symmetrize the tensor. Just put in the single term, as is.

for a, b, c, d in [
(p, q, r, s), (q, p, r, s),
(p, q, s, r), (q, p, s, r),
(r, s, p, q), (s, r, p, q),
(r, s, q, p), (s, r, q, p),
]:
two_body_tensor[a, b, c, d] = h_pqrs

# more terms
else:
raise ValueError(
"FermionOperator cannot be converted to MolecularHamiltonian."
f" The term {term} is neither a constant, one-body, or two-body "
"term."
)


return MolecularHamiltonian(
one_body_tensor=one_body_tensor,
two_body_tensor=two_body_tensor,
constant=constant,
)

def _approx_eq_(self, other, rtol: float, atol: float) -> bool:
if isinstance(other, MolecularHamiltonian):
Expand Down
22 changes: 22 additions & 0 deletions tests/python/hamiltonians/molecular_hamiltonian_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,25 @@ def test_rotated():
original_expectation = np.vdot(vec, linop @ vec)
rotated_expectation = np.vdot(rotated_vec, linop_rotated @ rotated_vec)
np.testing.assert_allclose(original_expectation, rotated_expectation)


def test_from_fermion_operator():
norb = 5
Copy link
Collaborator

Choose a reason for hiding this comment

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

Make the the test parametrized (see other tests for examples) and test a range of norb (0 to 4 should be good enough).


rng = np.random.default_rng()

# generate a random molecular hamiltonian
mol_hamiltonian = ffsim.random.random_molecular_hamiltonian(norb=norb, seed=rng)

# convert to fermion op
mol_hamiltonian_fops = ffsim.fermion_operator(mol_hamiltonian)

# convert back from fermion op to ham
mol_hamiltonian_from_ferm = ffsim.MolecularHamiltonian.from_fermion_operator(mol_hamiltonian_fops)


# check they are matching
np.testing.assert_allclose(mol_hamiltonian.constant, mol_hamiltonian_from_ferm.constant)
np.testing.assert_allclose(mol_hamiltonian.one_body_tensor, mol_hamiltonian_from_ferm.one_body_tensor)
np.testing.assert_allclose(mol_hamiltonian.two_body_tensor, mol_hamiltonian_from_ferm.two_body_tensor)

Loading