How to use the FermionOperator class¶
The FermionOperator class is used to represent a linear combination of products of fermionic creation and annihilation operators. For example:
Such operators are represented programmatically as a dictionary (hash table) that maps each term to its associated coefficient. An individual term is represented as a tuple of fermionic actions. Each action is described by three pieces of information:
Whether the action is to create or destroy a fermion.
The spin of the orbital being acted upon (alpha or beta).
The numerical index of the orbital.
Internally, the action is itself represented as a tuple containing these three pieces of information, but we recommend using the helper functions cre_a
, cre_b
, des_a
, and des_b
to construct these tuples.
The following code example shows how to construct the operator shown above.
[1]:
import ffsim
op1 = ffsim.FermionOperator(
{
(ffsim.cre_a(0), ffsim.des_a(3)): 0.5,
(ffsim.cre_a(3), ffsim.des_a(0)): -0.25,
(ffsim.cre_b(1), ffsim.des_b(5), ffsim.cre_a(4)): 1 + 1j,
}
)
op1
[1]:
FermionOperator({
(cre_a(3), des_a(0)): -0.25,
(cre_a(0), des_a(3)): 0.5,
(cre_b(1), des_b(5), cre_a(4)): 1+1j
})
Use repr
to view a string representation that displays the action tuples.
[2]:
repr(op1)
[2]:
'FermionOperator({((True, False, 3), (False, False, 0)): -0.25+0j, ((True, False, 0), (False, False, 3)): 0.5+0j, ((True, True, 1), (False, True, 5), (True, False, 4)): 1+1j})'
FermionOperators support arithmetic operations. Note that when multiplying a FermionOperator by a scalar, the scalar must go on the left, i.e. 2 * op
and not op * 2
.
[3]:
op2 = ffsim.FermionOperator(
{
(ffsim.cre_b(2),): 1j,
(ffsim.des_a(3), ffsim.des_b(3)): -0.25,
}
)
op3 = op1 * op2 + 2 * op1 - op2 / 4
op3
[3]:
FermionOperator({
(cre_b(2)): 0-0.25j,
(cre_a(3), des_a(0), cre_b(2)): 0-0.25j,
(cre_a(3), des_a(0), des_a(3), des_b(3)): 0.0625,
(des_a(3), des_b(3)): 0.0625,
(cre_a(0), des_a(3)): 1,
(cre_b(1), des_b(5), cre_a(4)): 2+2j,
(cre_b(1), des_b(5), cre_a(4), cre_b(2)): -1+1j,
(cre_a(3), des_a(0)): -0.5,
(cre_b(1), des_b(5), cre_a(4), des_a(3), des_b(3)): -0.25-0.25j,
(cre_a(0), des_a(3), cre_b(2)): 0+0.5j,
(cre_a(0), des_a(3), des_a(3), des_b(3)): -0.125
})
It is good to be aware that some in-place operations are especially efficient because they avoid copying data into a new FermionOperator object. These operations are:
In-place addition and subtraction
In-place multiplication and division by a scalar
Some examples of usage:
[4]:
op3 += op1
op3 -= op2
op3 *= 4
op3 /= 1j
op3
[4]:
FermionOperator({
(cre_b(2)): -5,
(cre_a(3), des_a(0), cre_b(2)): -1,
(cre_a(3), des_a(0), des_a(3), des_b(3)): 0-0.25j,
(des_a(3), des_b(3)): 0-1.25j,
(cre_a(0), des_a(3)): 0-6j,
(cre_b(1), des_b(5), cre_a(4)): 12-12j,
(cre_b(1), des_b(5), cre_a(4), cre_b(2)): 4+4j,
(cre_a(3), des_a(0)): 0+3j,
(cre_b(1), des_b(5), cre_a(4), des_a(3), des_b(3)): -1+1j,
(cre_a(0), des_a(3), cre_b(2)): 2,
(cre_a(0), des_a(3), des_a(3), des_b(3)): 0+0.5j
})
Operators can be normal-ordered by calling the normal_ordered
method, which returns a new FermionOperator. In the normal-ordered form of a FermionOperator, the operators comprising each term appear from left to right in descending lexicographic order by (action, spin, orb)
. That is, all creation operators appear before all annihilation operators; within creation/annihilation operators, spin beta operators appear before spin alpha operators, and larger orbital indices appear before
smaller orbital indices.
[5]:
op3.normal_ordered()
[5]:
FermionOperator({
(cre_a(3), des_b(3), des_a(3), des_a(0)): 0+0.25j,
(cre_b(1), cre_a(4), des_b(5), des_b(3), des_a(3)): -1+1j,
(cre_b(2), cre_a(0), des_a(3)): 2,
(cre_b(2), cre_a(3), des_a(0)): -1,
(cre_b(2)): -5,
(des_b(3), des_a(3)): 0+1.25j,
(cre_b(2), cre_b(1), cre_a(4), des_b(5)): 4+4j,
(cre_a(3), des_a(0)): 0+3j,
(cre_b(1), cre_a(4), des_b(5)): -12+12j,
(cre_a(0), des_a(3)): 0-6j
})
If a FermionOperator conserves particle number and the Z component of spin, then it can be converted to a LinearOperator using the ffsim.linear_operator
function. In order for a FermionOperator to conserve particle number and the Z component of spin, it must preserve the number of spin-alpha fermions and the number of spin-beta fermions. So far, the operators we have created do not satisfy
these criteria:
[6]:
print(op3.conserves_particle_number())
print(op3.conserves_spin_z())
False
False
Let’s construct a FermionOperator that does conserve particle number and spin, and then convert it to a LinearOperator within the subspace of 3 spatial orbitals with 1 alpha and 1 beta fermion.
[7]:
op4 = ffsim.FermionOperator(
{
(ffsim.cre_a(0), ffsim.des_a(3)): 1 + 1j,
(ffsim.cre_a(1), ffsim.des_a(1), ffsim.cre_b(1), ffsim.des_b(1)): -0.5,
}
)
print(op4.conserves_particle_number())
print(op4.conserves_spin_z())
norb = 3
nelec = (1, 1)
linop = ffsim.linear_operator(op4, norb=norb, nelec=nelec)
True
True
A LinearOperator can be matrix-multiplied onto a vector:
[8]:
vec = ffsim.random.random_state_vector(ffsim.dim(norb, nelec))
linop @ vec
[8]:
array([ 0. +0.j , 0. +0.j ,
0. +0.j , 0. +0.j ,
-0.06944034+0.086307j, 0. +0.j ,
0. +0.j , 0. +0.j ,
0. +0.j ])
It can also be passed into most linear algebra routines in scipy.sparse.linalg
.
[9]:
import scipy.sparse.linalg
eigs, vecs = scipy.sparse.linalg.eigs(linop, which="LM", k=1)
eigs
[9]:
array([-0.5+1.21984743e-18j])