Canonicalization¶
Symbolic tensor expressions in many-body physics are riddled with dummy (summed-over) indices, and the same physical term can be
written down with many different, equally valid choices of dummy index labels. Two sequant::Expr trees that are mathematically
equal are therefore not necessarily represented by identical trees — until they have been put into canonical form. Canonicalization
is what lets SeQuant recognize that two differently-labeled products are the same term (so they can be combined in a Sum), and it
underlies essentially every simplification performed throughout the library, from Wick’s theorem to
the coupled-cluster equation generator.
Why it matters¶
Consider a term such as \(\bar{g}^{a_1 a_2}_{i_1 i_2} t^{i_1 i_2}_{a_1 a_2}\). Relabeling the summed indices
(\(i_1 \leftrightarrow i_2\), \(a_1 \leftrightarrow a_2\)) yields an expression that is numerically identical, but whose
sequant::Product tree differs in the index labels attached to its sequant::Tensor factors. Without canonicalization,
adding this relabeled copy to the original would produce a two-term sequant::Sum instead of a single term with a factor of 2 —
and, more importantly, larger derivations (e.g. the hundreds of terms arising in a CCSDT residual) would never collapse to their true,
much smaller size.
// two Products that are mathematically identical, differing only by a
// relabeling of their (implicitly summed) dummy indices
auto term_a = ex<Tensor>(L"g", bra{L"i_1", L"i_2"}, ket{L"a_1", L"a_2"}) *
ex<Tensor>(L"t", bra{L"a_1", L"a_2"}, ket{L"i_1", L"i_2"});
auto term_b = ex<Tensor>(L"g", bra{L"i_2", L"i_1"}, ket{L"a_2", L"a_1"}) *
ex<Tensor>(L"t", bra{L"a_2", L"a_1"}, ket{L"i_2", L"i_1"});
// before canonicalization the two Products are not recognized as equal ...
SEQUANT_ASSERT(!(term_a == term_b));
// ... but canonicalize() puts both into the same normal form, using the
// tensor network machinery (which relies on the bundled bliss graph
// automorphism library) to consistently relabel dummy indices
term_a->canonicalize();
term_b->canonicalize();
SEQUANT_ASSERT(term_a == term_b);
How it works, briefly¶
Canonicalizing a single sequant::Tensor — putting its own bra/ket indices in a fixed order consistent with its declared
permutational symmetry — is handled by a sequant::TensorCanonicalizer, looked up by the tensor’s label in the current
sequant::Context (see Canonicalizer configuration). Canonicalizing a whole product of tensors (or of
normal-ordered operators) additionally requires choosing a consistent relabeling of the dummy indices shared between factors; SeQuant
does this by building a colored graph representation of the product (a tensor network) and computing its canonical form using the
bundled bliss graph-automorphism library. This machinery is what
Expr::canonicalize() invokes internally, and what sequant::simplify() combines with cheap algebraic clean-up (flattening,
dropping zeros, …) to fully reduce an expression. The method it uses and the indices it treats as named (external) are the
sequant::CanonicalizeOptions of the current Context; sequant::canonicalize() and sequant::simplify() take no
options of their own, so to canonicalize under other options scope a context that carries them (see Context and Configuration):
// simplify() combines canonicalization with cheap algebraic clean-up, so a
// Sum of the two forms above collapses to a single term with a factor of 2
auto sum = ex<Tensor>(L"g", bra{L"i_1", L"i_2"}, ket{L"a_1", L"a_2"}) *
ex<Tensor>(L"t", bra{L"a_1", L"a_2"}, ket{L"i_1", L"i_2"}) +
ex<Tensor>(L"g", bra{L"i_2", L"i_1"}, ket{L"a_2", L"a_1"}) *
ex<Tensor>(L"t", bra{L"a_2", L"a_1"}, ket{L"i_2", L"i_1"});
simplify(sum);
SEQUANT_ASSERT(sum.is<Product>());
std::wcout << sum << std::endl;
A fully canonicalized expression remembers that it is in canonical form, so canonicalizing or simplifying it again before it is modified does no work.
None of this needs to be invoked explicitly in typical use: the mbpt operator machinery and
CC equation generator call simplify()/canonicalization as needed while building up equations. Knowing that it happens —
and why two “different-looking” terms may in fact be the same one — is mainly useful for interpreting intermediate output and for
understanding why changing a tensor’s declared symmetry can change how many terms an equation prints as. The implementation details of
the tensor-network canonicalizer are documented separately, for contributors, in the developer guide.