diff --git a/SeQuant/core/expressions/tensor.hpp b/SeQuant/core/expressions/tensor.hpp index 07b3b7d5c2..bc74cb7dcd 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,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(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::Symm) { - braket_symmetry_ = BraKetSymmetry::Conjugate; - hermiticity_ = to_hermiticity(BraKetSymmetry::Conjugate); - } validate_indices(); - validate_symmetries(); + check_symmetries(); canonicalize_slots(); } @@ -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(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::Symm) { - braket_symmetry_ = BraKetSymmetry::Conjugate; - hermiticity_ = to_hermiticity(BraKetSymmetry::Conjugate); - } validate_indices(); - validate_symmetries(); + check_symmetries(); canonicalize_slots(); } @@ -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 { 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( 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(); 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); 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( 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()); 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 -" 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);