Skip to content

Symbolic conjugation redesign: canonical braket-conjugate fold, per-tensor conj report, core Re/Im algebra - #602

Open
kshitij-05 wants to merge 16 commits into
masterfrom
kshitij/feature/conjugation-symbolic
Open

Symbolic conjugation redesign: canonical braket-conjugate fold, per-tensor conj report, core Re/Im algebra#602
kshitij-05 wants to merge 16 commits into
masterfrom
kshitij/feature/conjugation-symbolic

Conversation

@kshitij-05

Copy link
Copy Markdown
Member

Supersedes the symbolic half of #591; evaluation/export changes follow in a separate PR.

Canonicalization

  • BraKetSymmetry::Conjugate tensors fold onto a single canonical bra/ket orientation: per-tensor in DefaultTensorCanonicalizer::canonicalize_braket (unfold-first, marker-convergent), and in TensorNetworkV3 where orientation is graph-dictated (content rule demoted to a tie-break on equal bra/ket space multisets), with a post-relabel refold so canonicalize is idempotent.
  • Conjugation is part of TN identity: conj markers are colored into create_graph bundles, gated by new braket_foldable / braket_conjugate_foldable predicates (c-number + symmetry checks in one place instead of five hand-rolled copies).
  • SlotCanonicalizationMetadata::conjugated_tensors (input ordinals) reports which tensors need conjugation, replacing the single conj parity bit (kept one cycle as [[deprecated]]; intended to dissolve into TreeIndex when it lands).
  • CanonicalizeSlotsOptions struct with defaulted named_index_compare; the 3-arg canonicalize_slots remains as a deprecated forwarder, in-tree call sites migrated.

Conjugation algebra

  • Core Re/Im expression nodes (core/expressions/complex.hpp) with eager rules: linearity, real-scalar hoist, i-rotation, composition table, conj action.
  • fold_conjugate_pairs: exact A + A* → 2·Re(A) / A − A* → 2i·Im(A), hash-bucketed matching, pluggable conjugate op; fold_conjugate_pairs_of_real_sum kept as a back-compat wrapper. Auto-fold in simplify() is behind SimplifyOptions::FoldConjugatePairs, default No until evaluators understand Re/Im.
  • Total sequant::conjugate(ExprPtr) (distributes over Product/Sum, rejects operator content) and is_hermitian_network().
  • Tensor::with_slots rebuild API carries conjugated() through tensor-rebuilding transforms (remove_spin, swap_bra_ket, expand_antisymm, swap_spin) — the root-cause class of earlier marker-loss bugs.

#591 review objections addressed: per-tensor conj report instead of one network-level bool; orientation dictated by the canonicalized graph; conj handled in regular canonicalization, not only canonicalize_slots; params bundled into an options struct with the foldability predicate.

Not in this PR: evaluation (lazy conj), export/ITF golden churn, anti-conjugate BraKetSymmetry for time reversal (TODO recorded at the predicate).

Tests: new tests/unit/test_conjugation.cpp catalogue (atoms, Sum/Product, TN, eval-boundary compat); full ctest green on Debug+THROW (246/246) and Release+THROW (279/279).

The loop started at named_indices.size(), assuming the leading edges are
the named ones. A named index that is not an edge (e.g. a pure proto
index) shifted that cutoff onto an anonymous edge; the skipped edge's
ordinal was then handed to another edge of the same space, yielding a
non-injective rewrite that duplicated a slot index. Latent until the
Conjugate braket fold reordered the edge sort.
Mirror Variable and Power: Tensor gains a conjugated_ marker -- in the
hash (contributing only when set, so unconjugated tensors hash
identically to before), in static_equal and static_less_than (T orders
before conj(T)), rendered as ^* on the label in to_latex and in the v1
serializer (label^*{...}, matching the Variable spelling; deserializer
grammar extension deferred -- conjugation currently arises only from
canonicalization at runtime, never from parsed input).

conjugate() toggles the marker and touches no slots; adjoint() is
deliberately unchanged (for BraKetSymmetry::Conjugate the swap IS the
adjoint, the conj being carried by the symmetry relation, so the marker
commutes through it -- certified by the new test).

This is the representation half of making the canonicalizer's
conjugation byproduct symbolic: instead of a network-level bool
(SlotCanonicalizationMetadata::conj / EvalExpr::canon_conj_), the
orientation fold will toggle conjugated_ on the tensor itself, giving
per-tensor granularity by construction.
…tarred spellings via EvalOp::Adjoint

EvalExpr(Tensor) no longer folds BraKetSymmetry::Conjugate leaves onto one
bra/ket orientation (flat: TensorBlockCanonicalizer with the fold disabled;
ToT: new CanonicalizeSlotsOptions/CreateGraphOptions::fold_conjugate_braket
knob giving orientation-sensitive bundle colors and no conj byproduct):
leaves keep their as-written spelling, so leaf yielders and evaluators need
no conjugation awareness. Folding fresh leaves onto one orientation-shared
cache slot is the lazy-conj eval follow-up; the conjugation marker still
colors the graph, so C*C vs C*·C stay distinct (the cache-aliasing fix).
An already-starred spelling is served through an explicit EvalOp::Adjoint
node over its unmarked value-orientation operand, mirroring the U+207A
adjoint-label channel.

Fixes the eval_with_tiledarray / ta_tot_adjoint_end_to_end CI failures;
catalogue tests updated to this contract.

@Krzmbrzl Krzmbrzl left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo, all non-template function implementations should go into the corresponding cpp file instead of in the header. Otherwise, we end up with headers coupled so strongly to each other that we start having to play tricks to use the productively.

Comment thread SeQuant/core/expressions/complex.hpp Outdated
Comment thread SeQuant/core/tensor_network/v3.cpp Outdated
Comment on lines +429 to +435
// a Conjugate half-tensor (empty bra or ket bundle) has no vertex for the
// empty bundle, so its recorded canonical bundle position is a
// value-initialized sentinel -- the graph cannot decide its orientation;
// the content-based refold after the lexicographic relabel decides it
if (braket_symmetry(tensor) == BraKetSymmetry::Conjugate &&
(bra_rank(tensor) == 0 || ket_rank(tensor) == 0))
continue;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this specific to conjugate braket symmetry? In other words, why does this not cause any issues for BraKetSymmetry::Symm?

@kshitij-05 kshitij-05 Sep 1, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it should be obvious that in BraKetSymmetry::Symm reorienting is not value-changing, but in Conjugate it is. I have updated the comments to explicitly state why this is...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we can't reorient T{a1;} to T{;a1}* though, which seems to be what this code prevents 👀

if (canonical_bra_ket_bundle_order[i][0] >
canonical_bra_ket_bundle_order[i][1]) {
canonical_bra_ket_bundle_order[i][1] &&
!ranges::equal(tensor._bra(), tensor._ket())) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this extra check guard against? Imo, we shouldn't try to be too clever at this point and just transfer all information from the graph canonicalization to the expression. Anything else always bears risk of breaking canonicalization due to introducing input-dependence of the "canonical" form.

@kshitij-05 kshitij-05 Sep 1, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a skip exactly to avoid input dependence. Can you explain more precisely what you mean by "just transfer all information from the graph canonicalization to the expression"?

How do you suggest we do tie-breaking over indistinguishable vertices, e.g., diagonal tensors with identical bra and ket colors when BraKetSymmetry::Conjugate? @evaleev

@Krzmbrzl Krzmbrzl Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain more precisely what you mean by "just transfer all information from the graph canonicalization to the expression"?

What I mean by that is that if the graph canonicalization indicates that we should swap bra and ket indices (which is what canonical_bra_ket_bundle_order[i][0] > canonical_bra_ket_bundle_order[i][1] checks for), we should do it without adding logic that only does it only sometimes. Aka.: Only take the information from the graph and don't add extra conditions.

How do you suggest we do tie-breaking over indistinguishable vertices, e.g., diagonal tensors with identical bra and ket colors when BraKetSymmetry::Conjugate?

This comes down to whether we write t{a1;a1} or t{a1;a1}*, right? Ideally, the graph would be constructed in a way that reflects this symmetry, then the graph canonicalization includes tie-breaking. Otherwise, we could check for this during the second-pass of canonicalization after graph-canonicalization and deterministically choose one of the two. This can even happen after index relabeling as the bra==ket property will be retained through that.

Comment thread SeQuant/core/tensor_network/v3.cpp Outdated
Comment on lines +651 to +659
// Re-apply the per-tensor braket orientation fold now that indices carry
// their FINAL labels: the fold's full-space-tie decision is
// label-sensitive, and a decision taken on pre-relabel labels need not
// be a fixed point of the relabeled expression. The fold is convergent
// (it decides on the VALUE orientation), so this pass makes the whole
// canonicalization idempotent.
for (auto &tensor_ptr : tensors_) {
DefaultTensorCanonicalizer::canonicalize_braket(*tensor_ptr);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wasn't this needed before (i.e. for BraKetSymmetry::Symm)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As stated in the previous comment reply

Comment thread SeQuant/domain/mbpt/spin.cpp Outdated
// Symm, keeping string fixtures coherent with ctor-built tensors.
const sequant::io::serialization::DeserializationOptions opts{
.def_perm_symm = sequant::Symmetry::Nonsymm,
.def_braket_symm = sequant::Hermiticity::Hermitian};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should apply hermitian symmetry by default. Afaik the only tensors for which this (almost) always applies are the integrals. The default braket symmetry will become Nonsymm once the default symmetry PR is merged.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is to be changed once #596 lands.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it needed before that? 👀

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did these have to be changed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tensor's symmetries were not explicitly mentioned. But configuring with a complex field now changes the values of the TN due to BraKetSymmetry::Conjugate. I believe this would be addressed in #596

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this is about the default symmetries used by deserialization, right?

Comment thread SeQuant/core/expressions/tensor.hpp Outdated
Comment on lines +756 to +768
/// @return whether this tensor is complex-conjugated elementwise (no slot
/// reordering; contrast adjoint(), which swaps bra and ket)
bool conjugated() const { return conjugated_; }

/// @brief complex-conjugates this tensor elementwise: toggles conjugated();
/// the slots are untouched. For a BraKetSymmetry::Conjugate tensor the
/// value identity T{q;p} = conj(T{p;q}) means a bra<->ket swap combined
/// with conjugate() preserves the represented value -- which is how the
/// canonicalizer folds the two orientations onto one spelling.
void conjugate() {
conjugated_ = !conjugated_;
reset_hash_value();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like adjoint needs to be interwoven with the conjugated flag.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have adjusted the docs. Although pure conj (without permutation) needs a separate flag, as it's not the same as adjoint

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By pure I mean elementwise conj

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree that pure conjugation needs to be tracked. What I meant to say was that when calling adjoint that flag should be toggled as well (at least for a complex-valued tensor). And I recall some place in the code where adjoint had to be followed by conjugate to achieve the desired effect (where I think just calling adjoint should be sufficient as it implies complex conjugation)

…s + sound fixes

Docs (the reviewer's why-only-for-Conjugate questions answered in place):
for Symm braket symmetry both orientations denote the SAME value, so
orientation decisions are label-independent and any spelling is harmless;
for Conjugate the orientations denote CONJUGATE values, so a swap must
toggle the elementwise-conjugation marker, sentinel/automorphic graph
verdicts must not be acted on, and the full-space-tie label tie-break makes
the post-relabel refold necessary. Also: vertex-painter comment explains why
the marker perturbs only marked tensors' colors (an unconditional combine
would reshuffle every marker-free network's canonical form); conj/swap/
adjoint algebra table moved into Tensor's class docs and adjoint() docs
spell out how it composes with conjugated(); catch2 braket fallback
documented as mirroring the ctor default; cost_analysis README explains the
explicit :N-N-S pins.

Code:
- drop SlotCanonicalizationMetadata::conj (no consumers since the eval
  boundary went fold-free); tests assert conjugated_tensors instead
- canonicalize_braket: single three-way space comparison
- mbpt::swap_bra_ket: in-place _swap_bra_ket on a copy instead of a
  with_slots rebuild
- complex.hpp: non-template implementations moved to new complex.cpp
- canonicalize_slots 3-arg forwarder un-deprecated (kept, documented)
PR #596 makes the programmatic Tensor ctor defaults fixed conservative
(Tensor::Defaults, NonHermitian) and moves the deserializer defaults into
the Context; braket symmetry is derived from hermiticity and base field.
Make this branch independent of the ambient deserializer defaults so the
two PRs compose in either order:

- ta_tot_adjoint_end_to_end: pin the ToT leaves' braket symmetry (:N-C-S)
  explicitly -- the test's premise is a Conjugate (Hermitian) leaf, not
  whatever the ambient default resolves to (identical behavior today)
- catch2 to_expression: concrete TODO(PR #596) on the Hermitian fallback
  (it mirrors the ctor default; flips or is dropped in lockstep)
- cost_analysis README: default-agnostic wording for the explicit :N-N-S
  pins (they state physical facts, independent of ambient defaults)

The remaining eval_node/tapp test sites added by this branch already pin
{.def_braket_symm = Hermiticity::NonHermitian} explicitly, matching #596's
future default.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Several core paths currently assume a conjugated tensor must be braket-Conjugate (and/or use Adjoint semantics), which can assert or mis-handle legitimate ^* usage and should be gated or generalized.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR redesigns SeQuant’s symbolic conjugation and braket-orientation canonicalization by (1) making conjugation a first-class tensor identity marker, (2) folding Conjugate bra/ket orientations into a canonical spelling (with per-tensor reporting), and (3) adding core symbolic Re/Im algebra plus exact folding of conjugate summand pairs.

Changes:

  • Canonical braket-conjugate folding (graph- and content-based), with per-tensor conjugated_tensors reporting and options-based canonicalize_slots.
  • Introduces first-class tensor conjugation marker ^* (parsing + serialization), propagation through rebuild/transforms, and graph coloring support.
  • Adds core RealPart/ImagPart nodes and fold_conjugate_pairs, plus targeted test updates and a new conjugation test catalogue.
File summaries
File Description
utilities/cost_analysis/README.md Documents explicit symmetry pinning in equation files to avoid default-Hermitian fold surprises.
utilities/cost_analysis/examples/df_r1.inp Pins tensor symmetries in example input to keep networks physically non-Hermitian.
utilities/cost_analysis/examples/ccsd_r2.inp Pins tensor symmetries in example input to keep networks physically non-Hermitian.
tests/unit/test_wick.cpp Updates expected canonical spellings to swapped+starred forms under new folding.
tests/unit/test_tensor.cpp Adds unit tests for tensor conjugation marker behavior (hash/order/serialize/adjoint).
tests/unit/test_tensor_network.cpp Updates network tests for V3-only support and conjugate-fold metadata expectations.
tests/unit/test_spin.cpp Introduces a real-orbital scoped context helper and updates spintrace expectations.
tests/unit/test_parse.cpp Adds parse/roundtrip coverage for t^*{...} tensor syntax.
tests/unit/test_mbpt.cpp Updates LaTeX/structure expectations for swapped+starred Hermitian spellings.
tests/unit/test_mbpt_cc.cpp Makes ext-index assertions robust to fold-induced bra/ket placement changes.
tests/unit/test_extract_subtrees.cpp Pins deserializer defaults to NonHermitian to keep tree-shape tests stable.
tests/unit/test_export.cpp Pins export test defaults to NonHermitian to prevent fold-driven reshaping.
tests/unit/test_export_python.cpp Pins tensor constructors/deserialization to NonHermitian for deterministic export fixtures.
tests/unit/test_eval_tapp.cpp Pins eval tests’ deserialization defaults to NonHermitian for stable IR shapes.
tests/unit/test_eval_ta.cpp Adds end-to-end adjoint serving coverage for starred ToT leaves.
tests/unit/test_eval_node.cpp Pins eval-node mechanics tests to NonHermitian and adds conjugate-fold mechanics section.
tests/unit/test_eval_expr.cpp Expands eval-boundary conjugation/fold behavior tests and aliasing regressions.
tests/unit/test_eval_btas.cpp Pins BTAS eval tests to NonHermitian defaults for stability.
tests/unit/test_conjugation.cpp New comprehensive test catalogue for conjugation identities, folding, and TN behavior.
tests/unit/test_canonicalize.cpp Updates canonicalization tests for explicit non-Hermitian tensors and new options APIs.
tests/unit/CMakeLists.txt Registers new unit test source test_conjugation.cpp.
tests/unit/catch2_sequant.hpp Aligns deserialize defaults with ctor defaults (Hermitian braket fallback).
SeQuant/domain/mbpt/spin.cpp Preserves conjugation marker through mbpt transforms (swap/remove spin/expand/swap spin).
SeQuant/domain/mbpt/rules/thc.cpp Normalizes folded tensors to value orientation before THC transform rebuilds.
SeQuant/domain/mbpt/rules/df.cpp Normalizes folded tensors to value orientation before DF transform rebuilds.
SeQuant/domain/mbpt/rules/csv.cpp Normalizes folded tensors to value orientation before CSV transform rebuilds.
SeQuant/domain/mbpt/biorthogonalization.cpp Migrates canonicalize_slots call to options struct form.
SeQuant/core/tensor_network/vertex_painter.hpp Adds optional conjugation coloring parameter to vertex painting.
SeQuant/core/tensor_network/vertex_painter.cpp Implements conditional conjugation coloring in vertex hashes.
SeQuant/core/tensor_network/v3.hpp Adds conjugated_tensors reporting and CanonicalizeSlotsOptions.
SeQuant/core/tensor_network/v3.cpp Implements conjugate-braket fold rules, idempotent refold, and metadata reporting.
SeQuant/core/tensor_canonicalizer.hpp Centralizes foldability predicates and adds fold_conjugate controls.
SeQuant/core/tensor_canonicalizer.cpp Implements canonical Conjugate folding with marker-convergent unfolding and tie-breaks.
SeQuant/core/options.hpp Adds SimplifyOptions::FoldConjugatePairs option.
SeQuant/core/options.cpp Implements SimplifyOptions::copy_and_set(FoldConjugatePairs).
SeQuant/core/optimize/single_term_detail.hpp Migrates canonicalize_slots to options struct usage.
SeQuant/core/io/serialization/v1/serialize.cpp Serializes tensor conjugation marker as label^*{...}.
SeQuant/core/io/serialization/v1/deserialize.cpp Parses optional ^* marker after tensor labels.
SeQuant/core/io/serialization/v1/ast.hpp Extends tensor AST node with conjugated flag.
SeQuant/core/io/serialization/v1/ast_conversions.hpp Applies parsed conjugation marker to constructed Tensor; asserts operators cannot be conjugated.
SeQuant/core/expressions/tensor.hpp Adds conjugation marker storage, LaTeX rendering, rebuild API, and value_oriented() helper.
SeQuant/core/expressions/expr_algorithms.hpp Declares conjugate, fold_conjugate_pairs, and is_hermitian_network APIs.
SeQuant/core/expressions/expr_algorithms.cpp Implements conjugation distribution, conjugate-pair folding, simplify hook, and hermitian-network check.
SeQuant/core/expressions/complex.hpp Introduces RealPart/ImagPart nodes and builders with eager rules.
SeQuant/core/expressions/complex.cpp Implements RealPart/ImagPart behavior, hashing, and eager builder logic.
SeQuant/core/expressions/abstract_tensor.hpp Adds virtual hooks for elementwise conjugation marker in AbstractTensor interface.
SeQuant/core/eval/eval_expr.cpp Disables conjugate fold at eval boundary, colors graphs by conjugation, and serves starred spellings via unary IR.
CMakeLists.txt Adds new core complex expression sources to the build.
Review details
  • Files reviewed: 48/48 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread SeQuant/core/eval/eval_expr.cpp
Comment thread SeQuant/core/expressions/expr_algorithms.cpp
Comment thread SeQuant/core/expressions/tensor.hpp
…onjugate fold

Addresses the PR review round: the marker (sequant::conjugate,
deserialized ^*) can land on leaves of any braket symmetry, but
value_oriented and binarize assumed it was only the Conjugate-fold
byproduct.

- value_oriented: total over the marker x braket-symmetry grid --
  Conjugate unfolds, Symm clears the value-redundant marker, Nonsymm
  throws std::logic_error (was an elidable SEQUANT_ASSERT that would
  otherwise silently swap slots)
- binarize(Tensor): marked-Nonsymm leaves refused up front, BEFORE the
  '⁺' label channel (which would otherwise serve a still-marked bare
  leaf, dropping the conjugation); marked-Symm leaves served unmarked;
  the Conjugate channel reuses value_oriented and shares one
  make_adjoint_node builder with the '⁺' channel
- leaf-hash rule centralized in hash_terminal_tensor: the marker enters
  the hash only where value-distinctive (Nonsymm), so t and t* no
  longer alias one cache slot; dead strip-for-hash copy in the EvalExpr
  ctor removed
- conjugate(Product): comment justifying Flatten::No (mirrors
  Product::clone)
- docs/includes: fold_conjugate_braket doc no longer names the deleted
  metadata.conj field; complex.hpp sheds constant.hpp; <compare> moved
  into the std include block

New regression tests: value_oriented_totality ([conjugation]) and the
'starred non-Conjugate leaves' section in test_eval_expr.
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.

3 participants