From 7cbc7992e44018b589f2cedb3029ad9f18867d08 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Mon, 15 Jun 2026 15:46:30 +0200 Subject: [PATCH 1/9] Make tensors non-symmetric by default As long as the calling site doesn't explicitly specify a symmetry or hermiticity, the tensor will be entirely non-symmetric. This conforms to what a typical user will expect. In case we do have a symmetry, we don't silently overwrite it, even if we know it doesn't make sense. Instead, throw a proper error informing the user that they can't do this. This way, they will always know what is happening. --- SeQuant/core/expressions/tensor.hpp | 45 ++++++----------------------- 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/SeQuant/core/expressions/tensor.hpp b/SeQuant/core/expressions/tensor.hpp index 07b3b7d5c2..0876736f70 100644 --- a/SeQuant/core/expressions/tensor.hpp +++ b/SeQuant/core/expressions/tensor.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -268,24 +269,7 @@ 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( @@ -302,9 +286,9 @@ class Tensor : public Expr, public AbstractTensor, public MutatableLabeled { // 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); + braket_symmetry_ != BraKetSymmetry::Nonsymm) { + throw Exception( + "(Anti)symmetrization operators must not have braket symmetry"); } validate_indices(); validate_symmetries(); @@ -323,18 +307,7 @@ 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( @@ -351,9 +324,9 @@ class Tensor : public Expr, public AbstractTensor, public MutatableLabeled { // 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); + braket_symmetry_ != BraKetSymmetry::Nonsymm) { + throw Exception( + "(Anti)symmetrization operators must not have braket symmetry"); } validate_indices(); validate_symmetries(); From 140e41d226af9c4ac2fd0f59161036953343f7b2 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Mon, 15 Jun 2026 16:42:17 +0200 Subject: [PATCH 2/9] Default to no symmetries when deserializing --- SeQuant/core/io/serialization/v1/deserialize.cpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/SeQuant/core/io/serialization/v1/deserialize.cpp b/SeQuant/core/io/serialization/v1/deserialize.cpp index f8e8ff7d2f..d9c07a7beb 100644 --- a/SeQuant/core/io/serialization/v1/deserialize.cpp +++ b/SeQuant/core/io/serialization/v1/deserialize.cpp @@ -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(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(); From 83eed70689fc4bad571f15bcab5c2baad874fefb Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Mon, 15 Jun 2026 16:42:41 +0200 Subject: [PATCH 3/9] Special handling of symm op symmetries in deserialization --- .../core/io/serialization/v1/ast_conversions.hpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/SeQuant/core/io/serialization/v1/ast_conversions.hpp b/SeQuant/core/io/serialization/v1/ast_conversions.hpp index 22e761c470..11a6e1f7e8 100644 --- a/SeQuant/core/io/serialization/v1/ast_conversions.hpp +++ b/SeQuant/core/io/serialization/v1/ast_conversions.hpp @@ -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: 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(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()); @@ -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( From 8dba518a9d6879e5c1cac580b3c2c43331dec1af Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Mon, 15 Jun 2026 16:43:09 +0200 Subject: [PATCH 4/9] Infer column symmetry instead of erroring --- SeQuant/core/expressions/tensor.hpp | 58 ++++++++++++----------------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/SeQuant/core/expressions/tensor.hpp b/SeQuant/core/expressions/tensor.hpp index 0876736f70..bc74cb7dcd 100644 --- a/SeQuant/core/expressions/tensor.hpp +++ b/SeQuant/core/expressions/tensor.hpp @@ -276,22 +276,8 @@ class Tensor : public Expr, public AbstractTensor, public MutatableLabeled { bra_, [](const Index &idx) { return static_cast(idx); })), ket_net_rank_(ranges::count_if( ket_, [](const Index &idx) { return static_cast(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::Nonsymm) { - throw Exception( - "(Anti)symmetrization operators must not have braket symmetry"); - } validate_indices(); - validate_symmetries(); + check_symmetries(); canonicalize_slots(); } @@ -314,22 +300,8 @@ class Tensor : public Expr, public AbstractTensor, public MutatableLabeled { bra_, [](const Index &idx) { return static_cast(idx); })), ket_net_rank_(ranges::count_if( ket_, [](const Index &idx) { return static_cast(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::Nonsymm) { - throw Exception( - "(Anti)symmetrization operators must not have braket symmetry"); - } validate_indices(); - validate_symmetries(); + check_symmetries(); canonicalize_slots(); } @@ -771,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 { From 18c15411c6bc1d9594d51b6f6bb6a3b293c0ee79 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Mon, 15 Jun 2026 16:43:33 +0200 Subject: [PATCH 5/9] Don't mark symm ops as hermitian --- SeQuant/domain/mbpt/context.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/SeQuant/domain/mbpt/context.cpp b/SeQuant/domain/mbpt/context.cpp index 0ef837bdb8..25655ef28b 100644 --- a/SeQuant/domain/mbpt/context.cpp +++ b/SeQuant/domain/mbpt/context.cpp @@ -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); From 83e0139267e176eef425e9c60da226a879ec546b Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Mon, 15 Jun 2026 16:43:59 +0200 Subject: [PATCH 6/9] Adapt parse tests to new symm handling --- tests/unit/test_parse.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_parse.cpp b/tests/unit/test_parse.cpp index 78d1c2d11a..f4256ad14f 100644 --- a/tests/unit/test_parse.cpp +++ b/tests/unit/test_parse.cpp @@ -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()); From 109aae35dbd138c42a629843f0202588d140c762 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Mon, 15 Jun 2026 16:44:17 +0200 Subject: [PATCH 7/9] Fix invalid symm op symmetry spec --- tests/unit/test_spin.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_spin.cpp b/tests/unit/test_spin.cpp index 24091c6a53..3660245d1e 100644 --- a/tests/unit/test_spin.cpp +++ b/tests/unit/test_spin.cpp @@ -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")); } @@ -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 -" From 4586be968e46bd793d6da001e83c4a5aae6371e8 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Mon, 15 Jun 2026 16:44:52 +0200 Subject: [PATCH 8/9] Parse biorth exprs as column symmetric --- tests/unit/test_biorthogonalization.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_biorthogonalization.cpp b/tests/unit/test_biorthogonalization.cpp index e62ef56ac1..8d5320d5a9 100644 --- a/tests/unit/test_biorthogonalization.cpp +++ b/tests/unit/test_biorthogonalization.cpp @@ -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); @@ -80,7 +81,8 @@ TEST_CASE("biorthogonalization", "[Biorthogonalization]") { container::svector expressions; container::svector expected; for (std::size_t k = 0; k < inputs.at(i).size(); ++k) { - ResultExpr parsed = deserialize(inputs.at(i).at(k)); + ResultExpr parsed = deserialize( + inputs.at(i).at(k), {.def_col_symm = ColumnSymmetry::Symm}); expressions.push_back(parsed); expected.push_back( @@ -108,7 +110,8 @@ TEST_CASE("biorthogonalization", "[Biorthogonalization]") { container::svector expressions; for (const std::wstring &str : current_inputs) { - expressions.push_back(deserialize(str)); + expressions.push_back(deserialize( + str, {.def_col_symm = ColumnSymmetry::Symm})); } REQUIRE_THROWS_WITH( From 7e867111e257d7b34c08eaca4519ba05aac822df Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Mon, 15 Jun 2026 16:45:26 +0200 Subject: [PATCH 9/9] Fix invalid symm op symmetry spec --- tests/unit/test_tensor_network.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_tensor_network.cpp b/tests/unit/test_tensor_network.cpp index 21aaa3c157..f4be9861eb 100644 --- a/tests/unit/test_tensor_network.cpp +++ b/tests/unit/test_tensor_network.cpp @@ -1831,12 +1831,11 @@ TEST_CASE("tensor_network_v3", "[elements][valgrind_skip]") { } SECTION("special") { - auto factors = - deserialize( - L"Ŝ{i_1;a_1}:N-C-S g{i_2,a_1;a_2,i_1}:N-C-S " - L"t{a_2;i_2}:N-C-S") - ->as() - .factors(); + auto factors = deserialize( + L"Ŝ{i_1;a_1} g{i_2,a_1;a_2,i_1}:N-C-S " + L"t{a_2;i_2}:N-C-S") + ->as() + .factors(); TN tn(factors);