Operator Interface for Symbolic Many-Body Algebra

Motivation

The mbpt::Operator class represents an abstract Fock-space many-body operator, such as the Hamiltonian operator, that can be used within SeQuant expressions. We already saw earlier that the lower-level tensor representation of such operators involves specific index labels which makes tensor representation inconvenient when multiple instances of the same operator occur. For example, consider a general 1-body operator

\[\hat{o}_1 \equiv o^{p_1}_{p_2} a_{p_2}^{p_1}.\]

The abstract representation \(\hat{o}_1\) is convenient for efficient algebraic manipulations such as expansion and simplication:

\[\left(1 + \hat{o}_1\right)^2 \equiv = 1 + \hat{o}_1 + \hat{o}_1 + \hat{o}_1^2 = 1 + 2 \hat{o}_1 + \hat{o}_1^2\]

The tensor form is less convenient for such manipulations for 2 reasons:

  • lowering multiple instances of \(\hat{o}_1\) such as in its square \(\hat{o}_1^2 \equiv \hat{o}_1 \hat{o}_1\) to the tensor form is context-sensitive since one must avoid index reuse:

    \[\hat{o}_1^2 = o^{p_1}_ {p_2} a_{p_2}^{p_1} o^{p_3}_{p_4} a_{p_4}^{p_3}\]
  • operating on tensor forms of many-body operators involves frequent use of canonicalization, such as to be able to simplify \(\hat{o}_1 + \hat{o}_1\) in the following tensor form

    \[\hat{o}_1 + \hat{o}_1 = o^{p_1}_ {p_2} a_{p_2}^{p_1} + o^{p_3}_{p_4} a_{p_4}^{p_3}\]

    to

    \[2 \hat{o}_1 = 2 o^{p_1}_ {p_2} a_{p_2}^{p_1}\]

    requires canonicalization of each term before attempting simplification. In the abstract form simplifying \(\hat{o}_1 + \hat{o}_1\) to \(2 \hat{o}_1\) is as simple as if \(\hat{o}_1\) were a scalar.

To be useful for more than just algebraic simplification the abstract operator form must include enough detail to determine what products of such operators do to quantum numbers (e.g., occupation numbers) of a Fock-space state. Each mbpt::Operator thus denotes its effect on the quantum numbers. This allows efficient evaluation of vacuum averages of products of mbpt::Operator objects by identifying only the non-zero contributions.

mbpt::Operator Anatomy

mbpt::Operator is constructed from 3 callables (e.g., lambdas):

  • void -> std::wstring_view: returns the operator label such as “T” or “Λ”

  • void -> ExptPtr: returns the tensor form of the operator, and

  • QuantumNumberChange<>& -> void: encodes the action on the given input state by mutating its quantum numbers given as the argument to the lambda.

The quantum numbers of a state and their changes are represented by the same class, mbpt::QuantumNumberChange. For the Fock-space states the quantum numbers are the number of creators/annihilators. In general context the number of creators/annihilators must be tracked separately for multiple IndexSpaces (e.g., for each hole and particle states in the particle-hole context). To support general operators which can create a variable number of particles and holes the numbers of creators/annihilators must be encoded by intervals. For example, if space \(\{p\}\) is a union of hole (fully occupied in the vacuum) space \(\{i\}\) and particle (fully empty in the vacuum) space \(\{a\}\) operator \(\hat{o}_1\) includes 4 components,

\[o^{p_1}_{p_2} a_{p_2}^{p_1} = o^{i_1}_{i_2} a_{i_2}^{i_1} + o^{i_1}_{a_2} a_{a_2}^{i_1} + o^{a_1}_{i_2} a_{i_2}^{a_1} + o^{a_1}_{a_2} a_{a_2}^{a_1},\]

each of which is has 1 (particle or hole) creator and 1 (particle or hole) annihilator. The net effect of this operator on a Fock-space state is to leave its number of hole/particle creators/annihilators unchanged or increased by 1. E.g., its effect on a state with \(n\) particle creators is produce a state with \([n,n+1]\) particle creators; similarly, action on a state with \([n,m]\) hole annihilators (\(n \leq m\)) will produce a state with \([n,m+1]\) hole annihilators.

Finally, for a product of mbpt::Operator it is possible to determine its effect on the quantum numbers of the input state using simple rules based on Wick’s theorem. This enables SeQuant to screen out expressions that would yield zero, before applying Wick’s theorem without much computational overhead. This logic can also be used to check if two operators commute with each other, which is crucial for reducing the expression to their compact canonical form.

The use of interval arithmetic for quantum number changes allows to treat operators in their natural compact form as long as possible. E.g., \(\hat{o}_1\) can be kept as a single term \(o^{p_1}_{p_2} a_{p_2}^{p_1}\) rather than having to decompose it into individual contributions, each with definite effect on quantum numbers. This is especially useful when the number of index spaces increases.

See also

  • Although the user can construct mbpt::Operator directly, SeQuant predefines factories for many commonly-used operators. For example, functions H_, T_, Λ_, R_, and L_ in the mbpt::op namespace all return instances of Operator (or expressions built from them), each representing a specific type of many-body operator (Hamiltonian, excitation, deexcitation, etc).

  • There are convenient helper functions available in sequant::mbpt namespace for constructing different types of QuantumNumberChange objects. For example, see sequant::mbpt::excitation_type_qns.

Examples

Let’s look at some examples using the following SeQuant context:

using namespace sequant;
using namespace sequant::mbpt;
set_default_context({.index_space_registry_shared_ptr = make_min_sr_spaces(),
                     .vacuum = Vacuum::SingleProduct});

Constructing an Operator

using sequant::mbpt::op_t;
using sequant::mbpt::qns_t;

// constructor takes label, tensor form and QuantumNumberChange
op_t fock_op([]() -> std::wstring_view { return L"f"; },
             []() -> ExprPtr {
               return ex<Tensor>(L"f", bra{L"p_1"}, ket{L"p_2"}) *
                      ex<FNOperator>(cre({L"p_1"}), ann({L"p_2"}));
             },
             [](qns_t& qns) { qns += sequant::mbpt::general_type_qns(1); });

The components of the Operator can be accessed as follows:

// get the label
auto label = fock_op.label();

// apply operator to another state
auto vac = qns_t{};  // vacuum state for example
auto modified = fock_op(vac);

// access the tensor form
auto fock_tensor = fock_op.tensor_form();

Expression construction and screening

using namespace sequant::mbpt::op;

// Construct a double excitation operator (T2)
auto T2 = T_(2);

// Construct a two-body Hamiltonian
auto H2 = H_(2);

// Product: H2 * T2 * T2
auto expr1 = H2 * T2 * T2;

// Screening: can an expression raise the vacuum to double excitation?
bool raises_to_double = raises_vacuum_to_rank(expr1, 2);  // true

// Screening: can an expression raise the vacuum up to double excitation?
bool raises_up_to_double = raises_vacuum_up_to_rank(expr1, 2);  // true

// Screening: can an expression lower a double excitation to the vacuum?
auto lambda2 = Λ_(2);
auto expr2 = lambda2 * H_(1);
bool lowers_to_vacuum = lowers_rank_to_vacuum(expr2, 2);  // true

Vacuum averaging and final expression

The sequant::mbpt::op::vac_av function can be used to compute the vacuum average of an operator level expression. If reference state differs from the Wick vacuum sequant::mbpt::op::ref_av function should be used instead to compute the reference average.

using namespace sequant::mbpt;

auto expr = op::H(2) * op::T(2) * op::T(2);
auto result = op::vac_av(op::P(2) * expr);
// vac_av is equivalent to ref_av for single-determinant reference:
// auto result = op::ref_av(op::P(2) * expr);

std::wcout << "Result: " << to_latex(result) << "\n";

Note

op::P is a projection operator which can be used to construct an excited bra or ket manifold based on the input.