Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 26 additions & 65 deletions SeQuant/core/expressions/tensor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <SeQuant/core/index.hpp>
#include <SeQuant/core/io/latex/latex.hpp>
#include <SeQuant/core/reserved.hpp>
#include <SeQuant/core/utility/exception.hpp>
#include <SeQuant/core/utility/macros.hpp>
#include <SeQuant/core/utility/string.hpp>
#include <SeQuant/core/utility/strong.hpp>
Expand Down Expand Up @@ -268,46 +269,15 @@ class Tensor : public Expr, public AbstractTensor, public MutatableLabeled {
ket_(make_indices(ket_indices)),
aux_(make_indices(aux_indices)),
symmetry_(s),
// Derive bra<->ket exchange symmetry from the indices' fields with a
// default Hermitian abstract trait, unless the caller explicitly
// requested a particular BraKetSymmetry (e.g. Nonsymm for amplitudes).
// base_field(bra_, ket_) is well-defined here: declaration order of the
// private members guarantees bra_/ket_ are constructed before
// braket_symmetry_.
// If the caller didn't specify a BraKetSymmetry: when at least one
// of bra/ket is nonempty, derive from base_field(bra_, ket_) +
// Hermitian (matches the field-agnostic Hermiticity-taking ctor).
// When both bra and ket are empty, the bra↔ket exchange has no
// physical meaning — fall back to the literal Conjugate default
// (historical Context::braket_symmetry() value); deriving Symm there
// would break the spintrace bookkeeping for vacuum-aux tensors.
braket_symmetry_(bks_opt.value_or(
(bra_.empty() && ket_.empty())
? BraKetSymmetry::Conjugate
: to_braket_symmetry(Hermiticity::Hermitian,
sequant::base_field(bra_, ket_)))),
braket_symmetry_(bks_opt.value_or(BraKetSymmetry::Nonsymm)),
hermiticity_(to_hermiticity(braket_symmetry_)),
column_symmetry_(ps),
bra_net_rank_(ranges::count_if(
bra_, [](const Index &idx) { return static_cast<bool>(idx); })),
ket_net_rank_(ranges::count_if(
ket_, [](const Index &idx) { return static_cast<bool>(idx); })) {
// The (anti)symmetrizer is a permutation-bookkeeping operator whose
// bra<->ket orientation defines/extracts the external indices and must be
// preserved; it must never be bra<->ket-symmetric (Symm), or
// canonicalization would swap its bra and ket and corrupt external-index
// extraction. Over a real field a Hermitian operator becomes Symm, so
// demote Symm to Conjugate here (Conjugate is treated as no-swap by the
// canonicalizer, matching the complex-field behavior) before
// canonicalize_slots() may act on it.
if ((label_ == reserved::antisymm_label() ||
label_ == reserved::symm_label()) &&
braket_symmetry_ == BraKetSymmetry::Symm) {
braket_symmetry_ = BraKetSymmetry::Conjugate;
hermiticity_ = to_hermiticity(BraKetSymmetry::Conjugate);
}
validate_indices();
validate_symmetries();
check_symmetries();
canonicalize_slots();
}

Expand All @@ -323,40 +293,15 @@ class Tensor : public Expr, public AbstractTensor, public MutatableLabeled {
ket_(std::move(ket_indices)),
aux_(std::move(aux_indices)),
symmetry_(s),
// If the caller didn't specify a BraKetSymmetry: when at least one
// of bra/ket is nonempty, derive from base_field(bra_, ket_) +
// Hermitian (matches the field-agnostic Hermiticity-taking ctor).
// When both bra and ket are empty, the bra↔ket exchange has no
// physical meaning — fall back to the literal Conjugate default
// (historical Context::braket_symmetry() value); deriving Symm there
// would break the spintrace bookkeeping for vacuum-aux tensors.
braket_symmetry_(bks_opt.value_or(
(bra_.empty() && ket_.empty())
? BraKetSymmetry::Conjugate
: to_braket_symmetry(Hermiticity::Hermitian,
sequant::base_field(bra_, ket_)))),
braket_symmetry_(bks_opt.value_or(BraKetSymmetry::Nonsymm)),
hermiticity_(to_hermiticity(braket_symmetry_)),
column_symmetry_(ps),
bra_net_rank_(ranges::count_if(
bra_, [](const Index &idx) { return static_cast<bool>(idx); })),
ket_net_rank_(ranges::count_if(
ket_, [](const Index &idx) { return static_cast<bool>(idx); })) {
// The (anti)symmetrizer is a permutation-bookkeeping operator whose
// bra<->ket orientation defines/extracts the external indices and must be
// preserved; it must never be bra<->ket-symmetric (Symm), or
// canonicalization would swap its bra and ket and corrupt external-index
// extraction. Over a real field a Hermitian operator becomes Symm, so
// demote Symm to Conjugate here (Conjugate is treated as no-swap by the
// canonicalizer, matching the complex-field behavior) before
// canonicalize_slots() may act on it.
if ((label_ == reserved::antisymm_label() ||
label_ == reserved::symm_label()) &&
braket_symmetry_ == BraKetSymmetry::Symm) {
braket_symmetry_ = BraKetSymmetry::Conjugate;
hermiticity_ = to_hermiticity(BraKetSymmetry::Conjugate);
}
validate_indices();
validate_symmetries();
check_symmetries();
canonicalize_slots();
}

Expand Down Expand Up @@ -798,11 +743,27 @@ class Tensor : public Expr, public AbstractTensor, public MutatableLabeled {
std::size_t bra_net_rank_;
std::size_t ket_net_rank_;

void validate_symmetries() {
// (anti)symmetric bra or ket makes sense only for particle-symmetric
// tensors
if (symmetry_ == Symmetry::Symm || symmetry_ == Symmetry::Antisymm)
SEQUANT_ASSERT(column_symmetry_ == ColumnSymmetry::Symm);
void check_symmetries() {
// The (anti)symmetrizer is a permutation-bookkeeping operator whose
// bra<->ket orientation defines/extracts the external indices and must be
// preserved; it must never be bra<->ket-symmetric (Symm), or
// canonicalization would swap its bra and ket and corrupt external-index
// extraction. Over a real field a Hermitian operator becomes Symm, so
// demote Symm to Conjugate here (Conjugate is treated as no-swap by the
// canonicalizer, matching the complex-field behavior) before
// canonicalize_slots() may act on it.
if ((label_ == reserved::antisymm_label() ||
label_ == reserved::symm_label()) &&
braket_symmetry_ != BraKetSymmetry::Nonsymm) {
throw Exception(
"(Anti)symmetrization operators must not have braket symmetry");
}

if (symmetry_ == Symmetry::Symm || symmetry_ == Symmetry::Antisymm) {
// (Anti)symmetry in bra and ket indices automatically implies column
// symmetry
column_symmetry_ = ColumnSymmetry::Symm;
}
}

hash_type memoizing_hash() const override {
Expand Down
15 changes: 7 additions & 8 deletions SeQuant/core/io/serialization/v1/ast_conversions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -283,14 +283,6 @@ struct Transformer {
auto [braIndices, ketIndices, auxiliaries] =
make_indices(tensor.indices, position_cache.get(), begin.get());

// braket_symm is a std::variant<BraKetSymmetry, Hermiticity>: a concrete
// BraKetSymmetry (a 'C'/'S'/'N' spec, or the default fallback) is forwarded
// verbatim, while a Hermiticity (an 'H'/'A' spec, or a Hermiticity-valued
// default) defers to the Tensor ctor, which resolves it against
// base_field(bra, ket) — matching the programmatic ex<Tensor>(label, bra,
// ket) default. The std::visit below dispatches on which alternative is
// held to the matching Tensor ctor overload (BraKetSymmetry- vs
// Hermiticity-taking).
auto [perm_symm, braket_symm, column_symm] =
to_symmetries(tensor.symmetry, default_symms.get(),
position_cache.get(), begin.get());
Expand Down Expand Up @@ -331,6 +323,13 @@ struct Transformer {
ann(std::move(braIndices)), vac);
}

// Set required symmetries for symmetrization operators
if (tensor.name == reserved::antisymm_label()) {
perm_symm = Symmetry::Antisymm;
} else if (tensor.name == reserved::symm_label()) {
column_symm = ColumnSymmetry::Symm;
}

// Dispatch to correct Tensor constructor (taking either BraKetSymmetry or
// Hermiticity)
return std::visit(
Expand Down
11 changes: 2 additions & 9 deletions SeQuant/core/io/serialization/v1/deserialize.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -246,15 +246,8 @@ AST parse(const StartRule &start, std::wstring_view input,

transform::DefaultSymmetries to_default_symms(
const DeserializationOptions &options) {
// Deserializer's BraKet fallback for legacy/short serialized forms that
// omit the braket spec. Matches the historical Context::braket_symmetry()
// default of Conjugate, preserving snapshot-test stability for existing
// serialized fixtures. Callers who want a specific fallback (e.g.
// Hermiticity::Hermitian to make deserialized expectations match the
// programmatic ex<Tensor>(label, bra, ket) default, or Nonsymm for
// amplitudes in CSE tests) set options.def_braket_symm explicitly.
transform::DefaultSymmetries symms{
Symmetry::Nonsymm, BraKetSymmetry::Conjugate, ColumnSymmetry::Symm};
transform::DefaultSymmetries symms{Symmetry::Nonsymm, BraKetSymmetry::Nonsymm,
ColumnSymmetry::Nonsymm};

if (options.def_perm_symm.has_value()) {
std::get<0>(symms) = options.def_perm_symm.value();
Expand Down
7 changes: 5 additions & 2 deletions SeQuant/domain/mbpt/context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,11 @@ OpClass to_op_class(const std::wstring& op) {
}

Hermiticity op_hermiticity(const std::wstring& op) {
// reserved labels are OpClass::gen, hence Hermitian by default
if (ranges::contains(reserved::labels(), op)) {
if (op == reserved::antisymm_label() || op == reserved::symm_label()) {
// Symmetrization operators are non-hermitian
return Hermiticity::NonHermitian;
} else if (ranges::contains(reserved::labels(), op)) {
// reserved labels are OpClass::gen, hence Hermitian by default
return default_hermiticity(OpClass::gen);
} else {
return get_default_mbpt_context().op_registry()->hermiticity(op);
Expand Down
9 changes: 6 additions & 3 deletions tests/unit/test_biorthogonalization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ TEST_CASE("biorthogonalization", "[Biorthogonalization]") {
for (std::size_t i = 0; i < inputs.size(); ++i) {
CAPTURE(i);

ExprPtr input_expr = deserialize(inputs.at(i));
ExprPtr input_expr =
deserialize(inputs.at(i), {.def_col_symm = ColumnSymmetry::Symm});

auto externals = external_indices(input_expr);

Expand Down Expand Up @@ -80,7 +81,8 @@ TEST_CASE("biorthogonalization", "[Biorthogonalization]") {
container::svector<ResultExpr> expressions;
container::svector<ResultExpr> expected;
for (std::size_t k = 0; k < inputs.at(i).size(); ++k) {
ResultExpr parsed = deserialize<ResultExpr>(inputs.at(i).at(k));
ResultExpr parsed = deserialize<ResultExpr>(
inputs.at(i).at(k), {.def_col_symm = ColumnSymmetry::Symm});
expressions.push_back(parsed);

expected.push_back(
Expand Down Expand Up @@ -108,7 +110,8 @@ TEST_CASE("biorthogonalization", "[Biorthogonalization]") {
container::svector<ResultExpr> expressions;

for (const std::wstring &str : current_inputs) {
expressions.push_back(deserialize<ResultExpr>(str));
expressions.push_back(deserialize<ResultExpr>(
str, {.def_col_symm = ColumnSymmetry::Symm}));
}

REQUIRE_THROWS_WITH(
Expand Down
5 changes: 2 additions & 3 deletions tests/unit/test_parse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -533,9 +533,8 @@ TEST_CASE("serialization", "[serialization]") {
REQUIRE(result.ket()[0].full_label() == L"e_1");
REQUIRE(result.ket()[1].full_label() == L"e_2");
REQUIRE(result.symmetry() == Symmetry::Antisymm);
// serialized form `R{...}:A` omits braket_symmetry; deserializer's
// legacy fallback is Conjugate (see v1/deserialize.cpp to_default_symms)
REQUIRE(result.braket_symmetry() == BraKetSymmetry::Conjugate);
REQUIRE(result.braket_symmetry() == BraKetSymmetry::Nonsymm);
// This column symmetry is implicit with Symmetry::Antisymm
REQUIRE(result.column_symmetry() == ColumnSymmetry::Symm);

REQUIRE(result.expression().is<Product>());
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_spin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1180,7 +1180,7 @@ SECTION("Closed-shell spintrace CCSDT terms") {
REQUIRE_THAT(
result,
EquivalentTo(
L"3 Ŝ{i_1,i_2,i_3;a_1,a_2,a_3}:N-C-S * "
L"3 Ŝ{i_1,i_2,i_3;a_1,a_2,a_3} * "
"g{a_1,a_2;a_4,a_5}:N-C-S * t{a_3,a_4,a_5;i_3,i_1,i_2}:N-C-S"));
}

Expand All @@ -1195,7 +1195,7 @@ SECTION("Closed-shell spintrace CCSDT terms") {
REQUIRE_THAT(
result,
EquivalentTo(
L"-6/5 Ŝ{i_1,i_2,i_3;a_1,a_2,a_3}:N-C-S * "
L"-6/5 Ŝ{i_1,i_2,i_3;a_1,a_2,a_3} * "
"g{a_1,a_2;a_4,a_5}:N-C-S * t{a_3,a_4,a_5;i_1,i_2,i_3}:N-C-S + "
"3 Ŝ{i_1,i_2,i_3;a_1,a_2,a_3}:N-C-S * g{a_1,a_2;a_4,a_5}:N-C-S *"
" t{a_3,a_4,a_5;i_3,i_1,i_2}:N-C-S -"
Expand Down
11 changes: 5 additions & 6 deletions tests/unit/test_tensor_network.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1831,12 +1831,11 @@ TEST_CASE("tensor_network_v3", "[elements][valgrind_skip]") {
}

SECTION("special") {
auto factors =
deserialize(
L"Ŝ{i_1;a_1<i_1>}:N-C-S g{i_2,a_1<i_1>;a_2<i_2>,i_1}:N-C-S "
L"t{a_2<i_2>;i_2}:N-C-S")
->as<Product>()
.factors();
auto factors = deserialize(
L"Ŝ{i_1;a_1<i_1>} g{i_2,a_1<i_1>;a_2<i_2>,i_1}:N-C-S "
L"t{a_2<i_2>;i_2}:N-C-S")
->as<Product>()
.factors();

TN tn(factors);

Expand Down
Loading