The Expression Tree

Every symbolic object that SeQuant manipulates — a tensor, a scalar, a sum, a product, an entire many-body equation — is represented as a node in a tree of sequant::Expr objects. Understanding this small, shared vocabulary makes the rest of the library predictable: the same handful of node types recur everywhere, from the simple examples in Getting started to the equations produced by the coupled-cluster machinery.

Expr and ExprPtr

sequant::Expr is the abstract base of every expression node; concrete node types (described below) derive from it. Expressions are always managed through sequant::ExprPtr, a smart pointer (interchangeable with std::shared_ptr<Expr>) that adds the arithmetic operators (+, -, *) used to build up expressions programmatically. An ExprPtr is itself iterable over its immediate subexpressions, so an expression tree can be walked, matched, or rewritten generically without knowing the concrete type of every node.

Leaves: Tensor, Constant, Variable

sequant::Tensor is the most common leaf: a labeled tensor quantity with a bra and a ket index set (and, optionally, auxiliary indices), carrying its own permutational and particle symmetry. Registering the index spaces those indices belong to is covered separately.

Scalar quantities are represented by two different leaf types, depending on whether their value is known while building the expression:

  • sequant::Constant holds a fixed complex-rational number, e.g. the \(\frac{1}{2}\) prefactor of a commutator expansion.

  • sequant::Variable holds a named scalar, such as a perturbation strength \(\lambda\), whose value is not fixed by the expression itself.

  • sequant::Power represents base^exponent for a rational exponent, where the base is a Constant or a Variable.

// Tensor is a leaf Expr: a labeled tensor with a bra and a ket index set
auto f = ex<Tensor>(L"f", bra{L"i_1"}, ket{L"a_1"});
auto g = ex<Tensor>(L"g", bra{L"i_1", L"i_2"}, ket{L"a_1", L"a_2"});

// ExprPtr arithmetic builds Product and Sum nodes; both are associative and
// flatten automatically, so nesting them never grows the tree depth
auto product = f * g;      // a Product of 2 factors
auto sum = f * g + g + f;  // a Sum of 3 summands, not a Sum of a Sum and f

SEQUANT_ASSERT(product.is<Product>());
SEQUANT_ASSERT(product.as<Product>().factors().size() == 2);
SEQUANT_ASSERT(sum.is<Sum>());
SEQUANT_ASSERT(sum.as<Sum>().summands().size() == 3);

std::wcout << sum << std::endl;

Composition: Product and Sum

sequant::Product (a scalar times zero or more factors) and sequant::Sum (zero or more summands) combine other expressions. Both are associative and flatten automatically: multiplying a Product into another Product, or adding a Sum into another Sum, does not create a deeper tree — the factors/summands are merged into the same node. A bare Constant factor is likewise folded straight into a Product’s scalar prefactor rather than kept as a separate factor. This is why the ordinary +/* operators on ExprPtr are usually all that is needed to build up even large equations: the tree stays as flat as possible without any manual bookkeeping.

// scalar leaves: Constant (a compile-time rational/complex number),
// Variable (a named runtime scalar), and Power (base^exponent, with base
// being a Constant or a Variable)
auto half = ex<Constant>(rational(1, 2));
auto lambda = ex<Variable>(L"\\lambda");
auto lambda_sq = ex<Power>(lambda, 2);

SEQUANT_ASSERT(lambda_sq.as<Power>().base() == lambda);
SEQUANT_ASSERT(lambda_sq.as<Power>().exponent() == 2);

auto scaled = half * g;  // Constant and Tensor combine into one Product
std::wcout << std::format(L"{}", scaled) << std::endl;

Note

Two expressions that are mathematically equal are not necessarily represented by identical trees until they have been put into canonical form; see Canonicalization.

ResultExpr: naming an equation

An sequant::Expr on its own has no notion of “what it is equal to” — it is only ever the right-hand side of an implicit equation. sequant::ResultExpr closes that gap by pairing an expression with an explicit left-hand side, either a sequant::Tensor (for a tensorial result such as a residual \(R^{a_1}_{i_1}\)) or a sequant::Variable (for a scalar result such as an energy). This is the representation handed to optimize() and to the code generators, since both need to know the external index pairing and symmetry of the result, not just the expression that computes it.

// ResultExpr pairs an expression with an explicit left-hand side, e.g.
// "R^{a_1}_{i_1} = f^{a_1}_{i_1}"; this is the object handed to optimize()
// and to the code generators in SeQuant/core/export
auto result = ResultExpr(Tensor(L"R", bra{L"i_1"}, ket{L"a_1"}), f);

SEQUANT_ASSERT(result.produces_tensor());
std::wcout << std::format(L"{}", result.expression()) << std::endl;

The examples print expressions with << and std::format, which SeQuant/core/io/format.hpp provides and which default to SeQuant’s text serialization format. Once built, an expression tree can be rendered to LaTeX or round-tripped through that format; see I/O for both and for the format specifiers that select between them.