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::Constantholds a fixed complex-rational number, e.g. the \(\frac{1}{2}\) prefactor of a commutator expansion.sequant::Variableholds a named scalar, such as a perturbation strength \(\lambda\), whose value is not fixed by the expression itself.sequant::Powerrepresentsbase^exponentfor a rational exponent, where the base is aConstantor aVariable.
// 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.