Skip to content

eval: add out-of-band SlotSymmetry descriptor carrier on EvalExpr (Phase 0)#567

Draft
evaleev wants to merge 11 commits into
masterfrom
feature/eval-slot-symmetry-carrier
Draft

eval: add out-of-band SlotSymmetry descriptor carrier on EvalExpr (Phase 0)#567
evaleev wants to merge 11 commits into
masterfrom
feature/eval-slot-symmetry-carrier

Conversation

@evaleev

@evaleev evaleev commented Jun 29, 2026

Copy link
Copy Markdown
Member

WARNING

This is an experimental sandbox playing in which should lead to sufficient understanding of the problem. The max goal is to figure out the algorithm for symmetry detection and min goal is to generate enough unit tests. The initial agentic-generated solution is incredibly naive, but hopefully subsequent iterations will produce something more robust.

Summary

Phase 0 of per-intermediate permutational-symmetry deduction: add the data carrier only, no deduction logic yet.

  • New SeQuant/core/eval/slot_symmetry.hpp: a SlotSymmetry descriptor struct
    • column_groups (over matched (bra[c], ket[c]) columns), bra_groups, ket_groups; each group carries a sign
    • empty() and an order-insensitive operator==
  • EvalExpr gains a default-constructed, out-of-band slot_symmetry_ member plus a slot_symmetry() accessor

Out-of-band constraint (encoding 1a)

The new field is write-default-only and is intentionally not referenced by any identity/serialization path: canonicalize_slots, EvalExpr hashing, the TensorNetwork bliss-graph builder, or export. No intermediate result tensor's symmetry tag is touched (make_tensor_wo_symmetries is unchanged). grep confirms slot_symmetry_/slot_symmetry() appear only in the three intended files. Later phases will populate and consume it.

Test plan

TDD: failing test first (RED: slot_symmetry.hpp not found), then implement (GREEN).

  • New tests/unit/test_slot_symmetry.cpp (tag [slot_symmetry]): default-empty descriptor, operator==, carrier present + empty on a leaf EvalExpr, and non-empty != empty. 5 assertions, pass.
  • Regression guard (Debug): [optimize],[EvalExpr],[EvalNode] -> 486 assertions in 11 test cases, all pass; [slot_symmetry] passes. (This branch also includes the [bubble] Debug-timeout fix as its parent commit, so the optimize suite now completes in Debug.)
  • Also rebuilt + ran clean in Release ([optimize] 323 assertions; [slot_symmetry],[EvalExpr],[EvalNode] 170 assertions).

@Krzmbrzl

Krzmbrzl commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Wouldn't it make more sense to make Tensor objects be able to store this partial symmetries (in addition to the current ones)? 'cause ultimately that's what we'll want and need for generalized symmetry support. If we introduce it as a separate property of an eval node, I can kinda see this becoming an issue once tensors are upgraded to general symmetries 🤔

In fact, the most reusable way to implement this is to make use of libPerm and just store a proper permutation group from which we can query the subset of permutation actions that we currently care about (or rather: know what to do with). This would probably maximize compatibility w.r.t. a future full symmetry generalization.

@evaleev

evaleev commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

Wouldn't it make more sense to make Tensor objects be able to store this partial symmetries (in addition to the current ones)? 'cause ultimately that's what we'll want and need for generalized symmetry support. If we introduce it as a separate property of an eval node, I can kinda see this becoming an issue once tensors are upgraded to general symmetries 🤔

When we are ready to support general symmetries it will make sense to reuse this in AbstactTensor API and Tensor.

In fact, the most reusable way to implement this is to make use of libPerm and just store a proper permutation group from which we can query the subset of permutation actions that we currently care about (or rather: know what to do with). This would probably maximize compatibility w.r.t. a future full symmetry generalization.

Presumably you mean symmetry is specified by a representation. I'm not sure over what actually (since numbers alone are not enough, presumably over operators that include complex conjugation etc.)

I do not think I'm able to bite all of this off at once. Consider this a modeling exercise that will inform future design decisions but meanwhile will be sufficient for some improvements. Since it took us 3 attempts to get TN canonicalizer, I assume this will take at least this many iterations.

@Krzmbrzl

Copy link
Copy Markdown
Collaborator

When we are ready to support general symmetries it will make sense to reuse this in AbstactTensor API and Tensor.

But why not put it there from the get-go? It seems that would implicitly also cover the eval expr case by means of every node having a result tensor.

Presumably you mean symmetry is specified by a representation. I'm not sure over what actually (since numbers alone are not enough, presumably over operators that include complex conjugation etc.)

No, I mean symmetry represented as (basically) a list of individual permutations (including sign). Not that much different from storing indices of permutable elements, really.

From a superficial glance, it seems like the new symmetry object introduced by this PR mostly re-invents what libPerm already does. Perhaps it would we worth it to have a quick in-person chat about this?

@evaleev

evaleev commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

But why not put it there from the get-go? It seems that would implicitly also cover the eval expr case by means of every node having a result tensor.

I do not have a complete picture in my mind of how these concepts interplay. E.g. what are the "column" arrangement of the intermediate's slots? Do we need to deduce the column structure of tensor network? etc.

No, I mean symmetry represented as (basically) a list of individual permutations (including sign). Not that much different from storing indices of permutable elements, really.

General symmetry handling will need to consider more general model than this (which is OK for real tensors with simple permutational symmetries), e.g. need for permutations to result in complex conjugation + nonabelian symmetries (in this case representation of the symmetry is not representable by plus or minus 1).

@Krzmbrzl

Copy link
Copy Markdown
Collaborator

Extension to complex conjugation is only an extension of the permutation class in libPerm. Also, libPerm doesn't make assumptions about Abelian-ness so non-abelian should not cause problems.

I do not have a complete picture in my mind of how these concepts interplay. E.g. what are the "column" arrangement of the intermediate's slots? Do we need to deduce the column structure of tensor network? etc.

With libPerm we could represent individual index permutations so the tensor layout doesn't have to be anything specific in order to represent what we might want to refer to as a "column symmetry" in any given context.

Base automatically changed from feature/result-shape-nonnull-pair-constraints to master June 30, 2026 16:46
evaleev added 3 commits June 30, 2026 12:58
…ase 0)

Introduce a SlotSymmetry descriptor struct (column_groups / bra_groups /
ket_groups, each a permutation group with a sign) and carry it on EvalExpr as
an out-of-band, default-constructed member with a slot_symmetry() accessor.

This is the data carrier only: no deduction logic, no mutators. The field is
write-default-only and is intentionally NOT referenced by any identity or
serialization path (canonicalize_slots, EvalExpr hashing, the TensorNetwork
bliss-graph builder, or export), and no intermediate result tensor's symmetry
tag is touched. Later phases will populate and consume it.
…hase 0)

Add from_leaf_tensor(Tensor const&), which maps a leaf tensor's permutational-
symmetry attributes onto a SlotSymmetry descriptor over its slot positions:
  - ColumnSymmetry::Symm   -> one ColumnGroup over the matched (bra[c],ket[c])
    columns (c in [0, min(bra_rank,ket_rank))), sign +1;
  - Symmetry::Symm/Antisymm -> a bra_group and a ket_group over the respective
    bundles (rank >= 2), sign +1/-1;
  - Nonsymm everywhere      -> empty.
Unpaired bra/ket slots and aux are never placed in a ColumnGroup.

The EvalExpr Tensor-leaf ctor now populates slot_symmetry_ from this. The field
remains out-of-band: nothing outside the unit test reads it, so leaf nodes that
now carry a non-empty descriptor do not perturb hashing, canonicalization, CSE,
or export (the existing eval/optimize suites pass unchanged).
…er (Phase 0)

Add deduce_slot_symmetry(left, right, result), realizing the spec column-group
inheritance rule: the result's matched columns form one ColumnGroup iff every
result-bra index traces to one operand's column-grouped slots, every result-ket
index traces to one operand's column-grouped slots, and the contraction between
those operands pairs symmetrically (the PPL / giant matched-pair-swap pattern).
Index tracing is by external label, sufficient for the Phase-0 acceptance cases.

Wire it into binarize's make_prod: the tensor*tensor branch sets the result
descriptor from deduce_slot_symmetry; the scalar*tensor branch and the trailing
scalar-multiplied-result branch pass the tensor operand's descriptor through
unchanged (a scalar factor preserves the slot layout). The descriptor is set via
a new EvalOpSetter::set_slot_symmetry mutator and remains out-of-band: it does
not touch the result tensor, its hash, canon indices, or connectivity graph, so
the eval/optimize suites pass unchanged.
@evaleev
evaleev force-pushed the feature/eval-slot-symmetry-carrier branch from b1b0f39 to 9c98fcd Compare June 30, 2026 16:59
@evaleev

evaleev commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

With libPerm we could represent individual index permutations so the tensor layout doesn't have to be anything specific in order to represent what we might want to refer to as a "column symmetry" in any given context.

it still seems that AbstractTensor/Tensor is a wrong abstraction here, as it only represents top-level (non-proto) slots in the PNO/CSV case. Here we presumably want to represent all slots (including those on CSV indices).

P.S. another potential concern raised by an agentic review: to deduce symmetry of a Sum we need to determine the intersection of group representations of multiple terms. It says this is nontrivial for a general permutation case assumed by libPerm.

…ity, merge-indices passthrough, proto-index aliasing)

C1 (Critical): column-group identity collapse
- SlotLoc::column_grouped bool replaced with optional<size_t> column_group_idx
- column_locations returns which operand ColumnGroup each column belongs to
- deduce_slot_symmetry clusters on 4-tuple (bra_supplier, bra_group_idx,
  ket_supplier, ket_group_idx) so columns from distinct source groups in the
  same operand are never merged into one oversized ColumnGroup

I1 (Important): passthrough descriptor survives merge_indices flattening
- Three sites in eval_expr.cpp (scalar*tensor, Sum make_sum, trailing scalar)
  previously copied the operand's descriptor unconditionally
- Fixed with a rank-match guard: only propagate when result bra_rank ==
  operand bra_rank && result ket_rank == operand ket_rank; under merge_indices
  both result ranks are 0 so the guard fires and the result gets an empty
  descriptor matching deduce_slot_symmetry behavior

I2 (Important): trace maps alias proto-indices
- All trace maps in slot_symmetry.cpp keyed on label() which strips the
  proto-index suffix; a_1<i_1> and a_1<i_2> collided under key "a_1"
- Fixed by switching all four maps (column_locations x2, rbra_pos/rket_pos
  builders, trace lambda) to full_label() keys; lookups and emplace are
  consistent on both sides

I3 (hardening): added comment at make_sum site documenting that intersect
relies on positional comparison and the I3 invariant holds because the Sum
result is built from left.canon_indices()

Tests added (test_slot_symmetry.cpp):
- C1 regression: 3-tensor outer-product (L*R)*P with L,R each ColumnSymm;
  verifies the result carries exactly 2 separate ColumnGroups of size 2
- I1 merge_indices: scalar*tensor with BinarizationOptions.merge_indices=true
  gives an empty descriptor
- I2 proto-index: L*R product where L's bra contains a_1<i_1> and a_1<i_2>;
  verifies the inherited bra_group has two distinct slot positions
- Symm(+1) leaf: Symmetry::Symm leaf gives bra_group and ket_group with sign +1
@evaleev
evaleev marked this pull request as draft July 1, 2026 11:56
evaleev added 2 commits July 1, 2026 14:27
…soundness)

Proto-indexed externals break the flat index->slot trace's bijectivity
assumption (an index also living inside another slot's proto-list couples
slots the trace treats as independent), which could yield a wrong symmetry.
deduce_slot_symmetry now declines (empty descriptor) when any operand or
result external carries proto-indices. Repeated identical factors are left
unguarded: they are a provable false negative (deduced group is always a
subset of the true symmetry), so a guard would only drop correct claims;
a test documents that incompleteness.
Records the corrected design for symmetry deduction (superseding the Phase-0
hand-rolled rules' approach): deduction via canonicalize_slots
canonicalize-and-compare probes on the TN colored graph (handles proto-index
dependencies and emergent/repeated-tensor symmetry that the flat trace cannot);
storage-sector canonicalization by block-sort / small-group orbit-min, NOT
Butler-Portugal (which solves the symbolic double-coset problem the TN
canonicalizer already replaces); target descriptor = signed permutation group +
optional conjugation character; mono-term in scope, multi-term/multidimensional-
irrep out of scope. Documents the interim proto-index soundness guard.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants