Context and Configuration¶
Many SeQuant operations — normal ordering, Wick’s theorem, the meaning of index labels, even how LaTeX output is typeset — depend on settings that are impractical to pass explicitly to every function call. SeQuant instead keeps this configuration in an implicit, global context: a global default that every function reads unless told otherwise. This page consolidates the two context objects a user configures and the recommended way to set them up for many-body/quantum-chemistry work; the step-by-step manual setup of index spaces is covered separately in Getting started.
The core Context¶
sequant::Context bundles the settings that give meaning to an expression: the sequant::IndexBasisRegistry (the
vocabulary of index spaces in use, e.g. occupied/virtual), the Vacuum relative to which operators are normal-ordered
(Vacuum::Physical — the true, particle-free vacuum —, Vacuum::SingleProduct — a single-determinant quasiparticle vacuum —, or
Vacuum::MultiProduct — a general reference state, for which sequant::WickTheorem applies the extended form of Wick’s
theorem, with density cumulants), and the SPBasis (spin-orbital vs. spin-free); whether a basis is orthonormal is a property
of the basis (sequant::IndexBasis::metric()), not of the context. It also owns the canonicalizer configuration. It is
accessed and replaced through sequant::get_default_context(), sequant::set_default_context(), and
sequant::reset_default_context().
A Context owns its registry, which its copies share and which cannot change while any context uses it: a registry given by value
is moved or copied in, and one given by std::shared_ptr is adopted if that is its only owner (e.g. a temporary, such as the result
of sequant::mbpt::make_sr_spaces(), or a moved-from pointer) and copied otherwise. Modifying a registry after giving it to a
context therefore does not affect the context, unless the registry was moved in or adopted and the caller kept another way to reach
it, such as a pointer to one of its spaces (see the warning of Context::set). To change the registry of a context, copy the
registry, modify the copy and set it on a copy of the context.
Constructing a Context from scratch and registering index spaces by hand, as shown in
Register Index Spaces, is the right approach when a custom vocabulary of index spaces is needed. For standard
quantum-chemistry conventions, sequant::mbpt::load() is a one-line shortcut: it builds a ready-made
sequant::IndexBasisRegistry for one of a few common conventions (sequant::mbpt::Convention — minimal,
single-reference, multi-reference, F12, …; sequant::mbpt::SpinConvention controls whether/how spin is tracked), sets it
and Vacuum::SingleProduct on a copy of the current default context, and installs that copy as the default context:
// the one-line way to configure SeQuant for standard single-reference
// quantum chemistry: populates the default IndexBasisRegistry with the
// usual occupied/virtual partitioning and switches the default Context to
// the single-product (quasiparticle) vacuum
mbpt::load(mbpt::Convention::SR, mbpt::SpinConvention::None);
const Context& ctx = get_default_context();
SEQUANT_ASSERT(ctx.vacuum() == Vacuum::SingleProduct);
SEQUANT_ASSERT(ctx.index_basis_registry() != nullptr);
Canonicalizer configuration¶
The Context owns the settings that govern how individual tensors are canonicalized (see Canonicalization):
the
sequant::TensorCanonicalizerobjects, keyed by tensor label (sequant::Context::set_tensor_canonicalizer()). When tensors are canonicalized as part of a product (a tensor network), a tensor uses the canonicalizer registered for its own label, if any (sequant::Context::nondefault_tensor_canonicalizer_ptr()), and otherwise the built-in one that sorts bra/ket indices according to the tensor’s symmetry. The canonicalizer keyed by the empty label (sequant::Context::tensor_canonicalizer_ptr()) is the oneTensor::canonicalize()applies to a lone tensor in which no index occurs more than once (one with a repeated index, protoindices included, is a tensor network and is canonicalized as one);the comparers that order indices and pairs of indices (
sequant::Context::set_index_comparer(),sequant::Context::set_index_pair_comparer());the cardinal tensor labels, which are given lexicographic preference during canonicalization (
sequant::Context::set_cardinal_tensor_labels()).
Canonicalization reads this configuration from the default context for Statistics::Arbitrary only: a canonicalized product
mixes tensors that have no statistics with operators of either statistics, and must order all of them consistently. The configuration
of a context installed for a specific statistics is currently ignored. Keep it identical to that of the Statistics::Arbitrary
context nonetheless (as sequant::set_scoped_modified_default_context() does), since a future version may consult the
statistics-specific context first.
These can be given up front through Context::Options or changed on an existing Context with the setters above; to change them
for the duration of a scope, use a scoped context (see below) with a modified canonicalizer configuration. The example
canonicalizes a product, since that is where a canonicalizer registered for a label is used (a sum canonicalizes each of its summands
separately):
// the Context also owns the canonicalizer configuration: within the scope,
// tensors labeled "A" are canonicalized by the null canonicalizer, so their
// slots keep the order the graph-based canonicalization of the product gives
// them, here the input order (hence no phase is produced); a lone
// Tensor::canonicalize() would use the canonicalizer of the empty label
// instead
auto make_product = [] {
return ex<Tensor>(L"A", bra{L"a_1", L"i_1"}, ket{L"i_2", L"a_2"},
Symmetry::Antisymm) *
ex<Tensor>(L"t", bra{L"a_3"}, ket{L"i_3"});
};
{
auto resetter = set_scoped_modified_default_context([](Context& scoped) {
scoped.set_tensor_canonicalizer(
L"A", std::make_shared<NullTensorCanonicalizer>());
});
auto product = make_product();
product->canonicalize();
SEQUANT_ASSERT(product.as<Product>().scalar() == 1);
}
// outside of the scope the default canonicalizer reorders the indices of A,
// picking up a sign
auto product = make_product();
product->canonicalize();
SEQUANT_ASSERT(product.as<Product>().scalar() == -1);
The canonicalizer configuration is part of the Context value, so installing a new Context replaces it: one constructed from
Context::Options or Context{}, or the one sequant::reset_default_context() restores, carries the default configuration.
Set the canonicalizers and labels on the Context you install, or derive it from the current one
(Context(get_default_context())). sequant::mbpt::load() does the latter: it sets only the index basis registry and the
vacuum on a copy of the current default context, so the rest of the configuration is kept. It installs the copy process-wide, hence
throws if the calling thread has a scoped context active, whose settings it would otherwise make permanent.
sequant::Context::set_cardinal_tensor_labels() takes the complete list of cardinal labels: the defaults (the reserved labels
for antisymmetrizers, symmetrizers and transpositions) are kept only if the list includes them, as the one returned by
sequant::mbpt::cardinal_tensor_labels() does.
Canonicalization options¶
The Context also carries the sequant::CanonicalizeOptions that sequant::canonicalize(),
sequant::simplify() and everything built on them use (Context::Options::canonicalization_options,
Context::set(CanonicalizeOptions), sequant::Context::canonicalization_options()): the canonicalization method, which
indices are named (external), and whether the labels of the named indices affect the result. Like the rest of the configuration
they are read from the Statistics::Arbitrary context only, by canonicalization and by the theorem machinery alike, whatever the
statistics of the expression. By default the named indices of an
expression are deduced as those that occur once in it; naming them explicitly makes an index that occurs more than once external,
which is what the theorem machinery relies on as well: the named indices of the context are the external indices of a
sequant::WickTheorem, the ones it does not sum over (an operator sequence given to it directly follows the same rule: an
index that appears in it twice is summed over unless the context names it). When the context names indices it names every
external index of the expressions canonicalized under it, since any other index is then a dummy. There are no per-call options;
to canonicalize under other options, scope a context that carries them (see below).
The MBPT Context¶
Everything under SeQuant/domain/mbpt (predefined operators, the CC equation generator, …) additionally consults a
second, MBPT-specific context, sequant::mbpt::Context. Its main content is an sequant::mbpt::OpRegistry: a map from
operator labels ("t", "f", "g", …) to their sequant::mbpt::OpClass (excitation, de-excitation, or general),
which is what lets SeQuant recognize e.g. that t is an excitation operator without inspecting its tensor form. It is configured
separately from the core Context, via sequant::mbpt::set_default_mbpt_context():
// most of SeQuant/domain/mbpt additionally needs an OpRegistry, which maps
// operator labels ("t", "f", "g", ...) to their OpClass (excitation,
// de-excitation, or general); this lives in a separate, mbpt-specific
// context that must be configured on top of the core one
mbpt::set_default_mbpt_context(
{.op_registry_ptr = mbpt::make_legacy_registry()});
SEQUANT_ASSERT(mbpt::to_op_class(L"t") == mbpt::OpClass::Ex);
Most MBPT functions assert that this has been configured; forgetting this step is a common source of “OpRegistry is null”-type errors when SeQuant code is first ported into a new program.
The CSV field (default CSV::No), when set to CSV::Yes, makes operator tensors built by
sequant::mbpt::OpMaker (e.g. cluster amplitudes t) use cluster-specific virtuals — virtual-space indices that carry the
operator’s occupied indices as proto-indices — instead of plain, independent virtual indices. sequant::mbpt::csv_transform()
expands such CSV-dependent tensors into an explicit basis (standard unoccupieds, PAOs, or AOs) when needed downstream; the
basis is given as an sequant::IndexSpace (its own, orthonormal basis), or as an sequant::IndexBasis
registered under a name, whose registry entry says whether it is orthonormal (sequant::IndexBasis::metric()).
// CSV controls whether OpMaker builds excitation/de-excitation operators
// (e.g. cluster amplitudes "t") with cluster-specific (index-dependent)
// virtuals or plain, independent ones; it defaults to CSV::No
auto csv_resetter = mbpt::set_scoped_default_mbpt_context(
{.csv = mbpt::CSV::Yes, .op_registry_ptr = mbpt::make_legacy_registry()});
SEQUANT_ASSERT(mbpt::get_default_mbpt_context().csv() == mbpt::CSV::Yes);
The sequant::mbpt::NormalizationConvention controls operator prefactors. Its default is Default;
Operator Interface for Symbolic Many-Body Algebra describes the two conventions and which operators each one affects.
Several bases of one space can meet in an expression: the canonical and the localized orbitals of a perturbation theory,
the cluster-specific virtuals of the ground-state and of the perturbed amplitudes, and so on. An sequant::Index
can therefore carry an optional basis instance, an opaque integer written after its proto indices, a_1<i_1,i_2;1>
(a_1<;1> without proto indices), unless the instance is registered under a name, which is then printed in place of
the space’s label (Register Index Spaces); an index without one is in its space’s own basis, as before.
The name is part of the basis (sequant::IndexBasis::name()), as a space’s label is of the space: an index parsed
from it or minted by SeQuant carries it, and so does one given the instance by number, which is resolved through the
registry. An index built from an sequant::IndexBasis with a bare instance number is in an unnamed basis of its
own, which is not the named one (the two indices are different), unless that basis is first looked up with
sequant::default_registry_resolved().
sequant::mbpt::add_ao_basis() registers the AOs this way, as named instances of the orbital spaces they span
with a general metric; sequant::mbpt::add_pao_basis() registers the PAOs as the named instance μ̃ of the
particle space (an index in it prints as μ̃_1), together with its α- and β-spin counterparts μ̃↑ and μ̃↓
of the spin-cased particle spaces, so that a PAO index can be spin-cased. The PAOs are the AOs projected on the particle
space, so the three follow the OBS AO basis μ for their extent, metric and field, which are set through μ’s
label; if μ is absent it is registered first, alone, so register the AO bases first when the VBS or ABS ones are
wanted.
Instances are granted per operator label and leg space with sequant::mbpt::OpRegistry::grant_basis(),
sequant::mbpt::OpMaker mints a granted operator’s legs with them, and the projectors of the CC
equations carry the grants of the amplitude being solved for. Integrals are never granted: in Wick’s theorem their legs
take the instance of the leg they are contracted with.
// several bases of one space can meet in one expression, e.g. canonical and
// localized occupied orbitals. An Index made from an IndexSpace runs over
// the space's own basis; one made from an IndexBasis with a basis instance
// runs over that basis of the space. The instance is written after the
// proto indices: i_1<;1>
const auto& isr = get_default_context().index_basis_registry();
const IndexSpace occ = isr->retrieve_space(L"i");
const Index i_canonical(occ, 1);
const Index i_localized(IndexBasis(occ, 1), 1);
SEQUANT_ASSERT(i_canonical.space() == i_localized.space());
SEQUANT_ASSERT(i_canonical != i_localized);
SEQUANT_ASSERT(i_localized.full_label() == L"i_1<;1>");
// a (de)excitation operator's legs take an instance through its registry
// entry: here the unoccupied legs of t are in basis instance 1, so the
// amplitudes OpMaker builds, and the projectors of the equations solved for
// them, carry it
auto granted_registry = mbpt::make_legacy_registry();
granted_registry->grant_basis(L"t", isr->retrieve_space(L"a"), 1);
auto grant_resetter = mbpt::set_scoped_default_mbpt_context(
{.op_registry_ptr = granted_registry});
const ExprPtr t1 = mbpt::op::tensor::t(1);
for (const Index& idx : get_used_indices(t1))
SEQUANT_ASSERT(idx.basis().basis_instance() ==
(isr->is_pure_unoccupied(idx.space())
? IndexBasis::optional_instance{1}
: IndexBasis::optional_instance{}));
Scoped context changes¶
Both context types support RAII-style, scoped overrides via sequant::set_scoped_default_context() (and its mbpt counterpart
sequant::mbpt::set_scoped_default_mbpt_context()): the returned resetter object restores the previous default context when it goes
out of scope, which is the safest way to temporarily change context for a single calculation without affecting surrounding code.
Scopes must end in the reverse order of their creation:
// temporarily switching context (e.g. to a spin-free basis for a single
// calculation) without disturbing the enclosing default: the RAII
// resetter restores the previous context when it goes out of scope
{
auto resetter = set_scoped_default_context({.spbasis = SPBasis::Spinfree});
SEQUANT_ASSERT(get_default_context().spbasis() == SPBasis::Spinfree);
}
SEQUANT_ASSERT(get_default_context().spbasis() == SPBasis::Spinor);
Note
Context::Options fields are independent of one another and of the previously active context: constructing a new Context
(whether directly or through the scoped-override helpers) starts from the library defaults and applies only the fields explicitly
given, rather than inheriting from whatever context happened to be active before.
To change a single field of the MBPT context, such as the normalization convention, copy the active context and set that field on the copy:
{
auto normalization_resetter = mbpt::set_scoped_default_mbpt_context(
mbpt::Context{mbpt::get_default_mbpt_context()}.set(
mbpt::NormalizationConvention::Symmetric));
SEQUANT_ASSERT(
mbpt::get_default_mbpt_context().normalization_convention() ==
mbpt::NormalizationConvention::Symmetric);
}
SEQUANT_ASSERT(mbpt::get_default_mbpt_context().normalization_convention() ==
mbpt::NormalizationConvention::Default);
Scoped contexts are per thread¶
A scoped context is seen only by the thread that installed it and by the parallel work it launches through SeQuant: the parallel
primitives of SeQuant/core/runtime.hpp (sequant::for_each and the others) make the calling thread’s scoped contexts current in
their workers for the duration of the call. Threads created by the user see the process-wide default context instead.
sequant::set_default_context() changes that process-wide default; a thread that has scoped contexts active sees the change only
after those scopes end.
Because it replaces the process-wide default, a call of sequant::set_default_context() on any thread invalidates the reference
that sequant::get_default_context() returns on a thread without scoped contexts, and anything obtained from that reference by
reference. Code that keeps the context beyond a brief read, as the canonicalizers do for the duration of a canonicalization, holds a
copy made by sequant::get_default_context_snapshot() instead. The copy reflects every change of the process-wide default
completed before the call and is cheap: it shares the index basis registry and the canonicalizer configuration with its source, and
takes the lock that guards the process-wide default only if that changed since the previous snapshot on the same thread.
sequant::get_default_index_basis_registry() reads only the index basis registry in the same way, without copying the context.
Code that reads the contexts at several points of one operation, as sequant::canonicalize(), sequant::simplify() and
WickTheorem::compute() do, pins them instead with sequant::pin_default_contexts(): on a thread without scoped contexts the
returned resetter scopes a copy of the contexts in effect, with their versions, so that a concurrent
sequant::set_default_context() is seen only once the pin ends; under scoped contexts, which cannot change under the caller, it
installs nothing.
Detecting changes¶
Every Context has a version (sequant::Context::version()) that identifies its canonicalization configuration: the index
space registry, the tensor canonicalizers and index comparers (compared as objects, not by behavior), the cardinal tensor labels, the
canonicalization options and the single-particle basis (which determines the symmetry of normal operators). Two contexts share a
version if and only if canonicalization sees the same configuration in both; a version is never reused for another configuration. The
other settings (vacuum, first dummy index ordinal, typesetting, deserialization defaults) do not affect it.
sequant::current_context_version() returns the version of the context in effect on the calling thread, which lets code that
caches canonicalization results tell whether they are still valid. Such a cache is keyed on the versions for all statistics, since
canonicalization reads the single-particle basis from the context for the statistics of the normal operator at hand and the rest
from the context for arbitrary statistics. The version does not track in-place mutation of a canonicalizer or comparer object that
the context refers to.