Fold BraKetSymmetry::Conjugate bra<->ket orientations onto one eval cache slot (opt-in) - #591
Fold BraKetSymmetry::Conjugate bra<->ket orientations onto one eval cache slot (opt-in)#591kshitij-05 wants to merge 5 commits into
Conversation
Opt-in (exploit_conjugate): canonicalize_slots reports whether folding the two bra<->ket orientations of a Hermitian (BraKetSymmetry::Conjugate) tensor onto one canonical orientation introduced a conjugation, so eval-node CSE can share one cache slot between the orientations and serve the swapped one via an adjoint. Default off; no behavior change without the opt-in.
Layer the exploit_conjugate byproduct from canonicalize_slots onto the eval tree, so the two bra<->ket orientations of a Conjugate leaf share one cached value. EvalExpr(Tensor, exploit_conjugate) threads the flag into the ToT leaf's canonicalize_slots call and records the conjugation in a new canon_conj() bit. That bit is kept OUT of hash_value() (only canon_phase is folded there), so a Conjugate leaf and its bra<->ket swap hash identically and share a cache slot. binarize(Tensor, opts) then turns the conjugated orientation into an EvalOp::Adjoint over the bare canonical leaf, carrying the SAME canonical index order -- so the existing adjoint evaluator's result(post) = operand(pre).conj() degenerates to a pure elementwise conjugation (post == pre, no transpose) on retrieval. This reuses the tested '+'-adjoint machinery rather than adding a new eval op. Default off leaves every existing path byte-identical: the new binarize branch is skipped and canonicalize_slots is called with exploit_conjugate=false, exactly as before. Test [exploit_conjugate] (replaces the throwaway probe): a proto-indexed Conjugate leaf and its adjoint fold to one hash with exactly one carrying the byproduct; binarize wraps the swapped orientation in EvalOp::Adjoint over the shared bare leaf with matching canonical indices; off by default keeps the two distinct.
Extend the exploit_conjugate conjugation channel to the flat
(protoindex-free) block-canonicalization leaf path, so a flat
BraKetSymmetry::Conjugate tensor and its bra<->ket-swapped partner fold
onto one cached value the same way the ToT/canonicalize_slots path
already does. This is the path flat complex-field Conjugate leaves take.
TensorBlockCanonicalizer::apply() already folds the two bra<->ket
orientations of a Symm tensor (a free relabeling). Factor that
color-based swap into a shared orient_braket_by_color() (apply()'s Symm
branch reuses it, byte-for-byte unchanged) and add
fold_conjugate_braket(), which applies the same swap to a Conjugate
tensor and reports whether it swapped -- for Conjugate the swap carries a
conjugation (C{ket;bra} = conj(C{bra;ket})), so it is a byproduct the
caller must consume, not a free relabeling.
The EvalExpr flat-leaf ctor branch calls it under exploit_conjugate and
records the result in canon_conj_ (kept out of the hash, so the two
orientations share a cache slot); binarize(Tensor)'s existing
EvalOp::Adjoint wrap then serves the swapped orientation as a pure
elementwise conjugation on retrieval, exactly as for the ToT path.
Default off leaves every existing path byte-identical. Limitation:
equal-color bra/ket bundles (identical spaces) are not folded on the flat
path -- that needs a full index-pattern comparison, which only the
bliss/ToT path does; the flat color rule matches apply()'s Symm fold.
Test [exploit_conjugate] gains a flat-leaf section (C{a_1;i_1}:N-C-S)
mirroring the ToT checks: off -> distinct, on -> fold + exactly one
conjugated + Adjoint over the shared bare leaf.
EvalExpr(Tensor)'s canonicalize_slots call passed {} for
named_index_compare in order to reach the exploit_conjugate argument. An
empty comparator is NOT the declared default: canonicalize_slots then
falls back to an internal space()-only lambda, whereas the declared
default (default_idxptr_slottype_lesscompare) orders named indices by
proto-index count first. That proto-count-first order is what lays a
proto-indexed (ToT) coefficient's canon_indices out with occupieds
first -- a layout downstream coefficient-shape detectors rely on. So {}
silently mis-ordered them and broke such consumers.
Pass default_idxptr_slottype_lesscompare{} explicitly, restoring the
comparator every ToT leaf had before the exploit_conjugate arg was
threaded. Flat leaves (block-canon else-branch) are unaffected.
The flat-leaf exploit_conjugate channel folds the two bra<->ket orientations of a Conjugate tensor onto one cache slot, serving the swapped orientation via EvalOp::Adjoint. The ToT (proto-indexed) leaf path lacked the fold: TNV3's canonicalize_slots does not exploit conjugate braket symmetry, so the orientations landed in separate slots. Apply TensorBlockCanonicalizer::fold_conjugate_braket to the ToT leaf before the network canonicalization (the color rule is label-independent and proto-safe) and compose the byproduct into canon_conj_; the ToT TA Result backend already implements adjoint() (conj recurses into nested tiles). Off by default (exploit_conjugate opt-in unchanged). Test tot_conjugate_braket_fold: hashes fold, canon_conj marks the swapped orientation, no behavior change without the opt-in.
There was a problem hiding this comment.
Pull request overview
This PR introduces an opt-in exploit_conjugate path that canonicalizes BraKetSymmetry::Conjugate tensors so bra↔ket-swapped orientations share a single eval/cache identity, recording a conjugation “byproduct” and serving the swapped orientation via EvalOp::Adjoint.
Changes:
- Add
exploit_conjugateplumbing toTensorNetworkV3::canonicalize_slots()and graph construction, plus a network-levelconjbyproduct bit. - Extend
EvalExprwithcanon_conj()(excluded from hashing) and updatebinarize()to wrap conjugated-orientation leaves withEvalOp::Adjointwhen opted in. - Refactor tensor block canonicalization to factor out bra/ket orientation logic and add
fold_conjugate_braket(), with new unit tests covering flat and ToT leaf paths.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_tensor_network.cpp | Adds coverage for exploit_conjugate behavior in TN canonicalization metadata and hashing. |
| tests/unit/test_eval_expr.cpp | Adds end-to-end eval/binarize tests asserting leaf folding and Adjoint wrapping behavior when opted in. |
| tests/unit/test_canonicalize.cpp | Adds a ToT-focused regression test validating Conjugate folding behavior and opt-in default-off behavior. |
| SeQuant/core/tensor_network/v3.hpp | Extends canonicalization API/metadata with exploit_conjugate and conj byproduct reporting. |
| SeQuant/core/tensor_network/v3.cpp | Implements Conjugate bra/ket folding in graph coloring and computes the conjugation byproduct. |
| SeQuant/core/tensor_canonicalizer.hpp | Declares fold_conjugate_braket() and an extracted bra/ket orientation helper. |
| SeQuant/core/tensor_canonicalizer.cpp | Implements the extracted orientation logic and Conjugate fold helper; keeps Symm behavior unchanged. |
| SeQuant/core/eval/eval_expr.hpp | Adds EvalExpr(Tensor,bool) doc and canon_conj() API; extends BinarizationOptions. |
| SeQuant/core/eval/eval_expr.cpp | Implements canon_conj_ propagation, passes exploit_conjugate into TN canonicalization, and wraps swapped Conjugate leaves via EvalOp::Adjoint in binarize(). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| #include <initializer_list> | ||
| #include <iostream> | ||
| #include <memory> |
| auto md = [&cardinal](const std::wstring& s, bool exploit) { | ||
| TN tn(deserialize(s)); | ||
| return tn.canonicalize_slots(cardinal, nullptr, {}, exploit); | ||
| }; |
| // Opt-in: fold the two bra<->ket orientations of a ToT Conjugate leaf | ||
| // onto one canonical orientation BEFORE the network canonicalization | ||
| // (TNV3 does not exploit conjugate braket symmetry itself), recording | ||
| // the conjugation byproduct exactly like the flat-leaf branch below. |
Krzmbrzl
left a comment
There was a problem hiding this comment.
I feel like exploitation of conjugate BraKet symmetry should be on-by-default. That would be consistent with how we handle all other symmetries.
Also, I don't think tracking the conjugation result by means of a boolean variable is the right way to go. At some point in the future (ideally already within this PR) we want to handle this to full extent implying that only some tensors might be conjugated whereas others might not. Hence, we need a more granular result telling us exactly which tensors need conjugation. Otherwise, we will have to break the public API eventually to account for this.
I have implemented a TreeIndex class on a different branch that allows uniquely identifying an element in a (potentially nested) (expression) tree. We could use that and then return a vector of TreeIndex objects to specify which tensors need conjugation. This would have the advantage that this interface would still be usable once the canonicalizer can deal with nested expressions.
| /// T{bra;ket} = conj(T{ket;bra}), so folding its two orientations onto one | ||
| /// canonical form carries a conjugation, recorded here (cf. `phase`, which | ||
| /// carries the ±1 linear byproduct of antisymmetric slot reorderings). | ||
| bool conj = false; |
There was a problem hiding this comment.
I feel like a simple boolean flag is insufficient as it might only be a single tensor that needs conjugation whereas others are left unchanged.
There was a problem hiding this comment.
It seems like support for conjugate canonicalization is only added to canonicalize_slots which seems to imply that regular canonicalization still doesn't handle it. If this is true, I think this is inconsistent and should be changed to also support it in regular canonicalization routines.
| // (canonical_bra_ket_bundle_order, v3.cpp above). N.B. metadata.conj is a | ||
| // single network-level bit: rigorous for one Conjugate tensor (the | ||
| // proto-indexed-leaf case that eval-node identities canonicalize), the | ||
| // per-leaf conjugation of a multi-Conjugate-tensor network is future work. |
There was a problem hiding this comment.
is that actually much more complicated? If not, it would make sense to just do the full thing here 🤔
| SlotCanonicalizationMetadata canonicalize_slots( | ||
| const container::vector<std::wstring> &cardinal_tensor_labels = {}, | ||
| const NamedIndexSet *named_indices = nullptr, | ||
| SlotCanonicalizationMetadata::named_index_compare_t named_index_compare = | ||
| default_idxptr_slottype_lesscompare{}); | ||
| default_idxptr_slottype_lesscompare{}, | ||
| bool exploit_conjugate = false); |
There was a problem hiding this comment.
I think it's time to bundle these options into an options struct and pass that instead. The number of parameters starts becoming unwieldy 👀
| auto bra = mutable_bra_range(t); | ||
| auto ket = mutable_ket_range(t); |
There was a problem hiding this comment.
Why does this need mutable_*? It seems like they aren't actually mutated
| for (auto&& idx : bra) bra_spaces.push_back(idx); | ||
| for (auto&& idx : ket) ket_spaces.push_back(idx); |
There was a problem hiding this comment.
Using universal reference is unnecessary if we don't forward the indices
| for (auto&& idx : bra) bra_spaces.push_back(idx); | |
| for (auto&& idx : ket) ket_spaces.push_back(idx); | |
| for (const auto& idx : bra) bra_spaces.push_back(idx); | |
| for (const auto& idx : ket) ket_spaces.push_back(idx); |
| // in via exploit_conjugate. Default keeps Conjugate bra/ket distinctly | ||
| // colored (no fold, no conj), mirroring historical behavior. | ||
| const auto cardinal = TensorCanonicalizer::cardinal_tensor_labels(); | ||
| auto md = [&cardinal](const std::wstring& s, bool exploit) { |
There was a problem hiding this comment.
md seems like a very non-descriptive name. It's short but also doesn't hint at what the function is doing. I'd recommend a more readable/speaking function name
A Hermitian (
BraKetSymmetry::Conjugate) tensor and its bra↔ket-swapped partner are the same data up to complex conjugation, but the eval layer treated the two orientations as distinct leaves/subtrees, evaluating and caching both. This adds an opt-inexploit_conjugatechannel that folds the orientations onto one canonical form:TensorNetworkV3::canonicalize_slotsgains anexploit_conjugateargument and reports the conjugation byproduct of the orientation fold;EvalExprrecords the byproduct in a newcanon_conj()bit, kept out ofhash_value()so both orientations share a cache slot;binarizeserves the swapped orientation via the existingEvalOp::Adjointmachinery (pure elementwise conjugation on retrieval — both TA backends, flat and ToT, already implementResult::adjoint);TensorBlockCanonicalizerfactors its Symm-branch orientation choice intoorient_braket_by_color()and addsfold_conjugate_braket()for the Conjugate case (flat and ToT leaf paths);default_idxptr_slottype_lesscompare{}named-index comparator instead of{}(which silently selected the space-only fallback and mis-ordered proto-indexed canon_indices).Default off; no behavior change without the opt-in. Covered by
[exploit_conjugate]unit tests for both flat and ToT leaves; the full unit suite passes (311243 assertions in 88 cases).