Skip to content

core: BraKetSymmetry of a Tensor deduced from indices' field + Hermiticity; drop Context::braket_symmetry#539

Open
evaleev wants to merge 8 commits into
masterfrom
evaleev/feature/tensor-bks-from-field-and-hermiticity
Open

core: BraKetSymmetry of a Tensor deduced from indices' field + Hermiticity; drop Context::braket_symmetry#539
evaleev wants to merge 8 commits into
masterfrom
evaleev/feature/tensor-bks-from-field-and-hermiticity

Conversation

@evaleev
Copy link
Copy Markdown
Member

@evaleev evaleev commented Jun 3, 2026

Summary

A tensor's bra↔ket exchange symmetry is its own physical fact — the
composition of its abstract Hermiticity with the scalar field over which
its bra/ket vector pairing is defined (base_field(bra, ket)). It is not
a property of the active Context. This PR removes Context::braket_symmetry
and teaches the legacy Tensor ctors to derive their BraKetSymmetry default
the same way the field-agnostic Hermiticity-taking ctors already do.

What changes

1. Tensor ctors: default bks deduced from indices, not Context (86a613b3c)

The 6 legacy Tensor ctors (the two primary reserved_tag ones + 4 public
delegators) previously defaulted BraKetSymmetry bks to
get_default_context().braket_symmetry(). They now take
std::optional<BraKetSymmetry> bks_opt = std::nullopt and the primary ctors'
init lists resolve it via

braket_symmetry_(bks_opt.value_or(to_braket_symmetry(
    Hermiticity::Hermitian, sequant::base_field(bra_, ket_))))

Member declaration order guarantees bra_/ket_ are constructed before
braket_symmetry_, so reading them in the init expression is well-defined.

Callers that pass an explicit BraKetSymmetry (e.g. BraKetSymmetry::Nonsymm
for amplitudes) implicitly construct the optional and behave as before.
Callers that previously relied on the Context-derived default (C tensors
built by csv_transform_impl, overlap factories, etc.) now derive directly
from the indices' fields:

  • real field → Symm
  • complex field → Conjugate

2. Remove Context::braket_symmetry (69b11265c)

Removals:

  • Context::braket_symmetry() accessor and braket_symmetry_ member
  • Context::Options::braket_symmetry field
  • Context::Defaults::braket_symmetry constant
  • Context::set(BraKetSymmetry) setter
  • braket_symmetry() comparison in Context::operator==

Updates to the remaining users:

  • v1/deserialize.cpp::to_default_symms: fallback initialized to
    BraKetSymmetry::Conjugate (later relaxed — see commit 3).
  • v3.cpp dummy-edge strict-assert: the per-edge upper bound on bra/ket
    endpoints can no longer be selected by a single Context value (per-tensor
    BraKetSymmetry would require per-vertex resolution); uses the looser
    nbra + nket <= 2 bound that subsumes both old branches.
  • Tests: drop the braket_symmetry option / set(BraKetSymmetry::Symm) chain
    links in test_runtime, test_canonicalize, test_main, test_wick,
    benchmarks/main.cpp.

3. Deserialization fallback matches the new programmatic default (50513fd76)

The deserializer's BraKet fallback (when the serialized form omits it and
the caller didn't pin DeserializationOptions::def_braket_symm) should match
the programmatic ex<Tensor>(label, bra, ket) default — otherwise the same
expression built two ways ends up with two different braket_symmetry_
values and the ResultExprs compare unequal.

Change:

  • v1/ast_conversions.hpp: DefaultSymmetries' BraKet slot is now
    std::optional<BraKetSymmetry>. std::nullopt means "defer to the
    Tensor ctor's derive-from-indices+Hermitian default". to_symmetries
    returns the optional through to the ex<Tensor> call.
  • v1/deserialize.cpp: to_default_symms initializes BraKet to
    std::nullopt. Callers who want a fixed fallback (e.g. CSE tests using
    BraKetSymmetry::Nonsymm for amplitudes) still set
    options.def_braket_symm explicitly and that wins, unchanged.

Test snapshot rebaselines:

  • test_mbpt: C{;;x_1,x_2} now spelled :N-S-S — empty bra/ket ⇒ Real
    field ⇒ Symm under the new default.
  • test_wick: the two -1/2 terms swap order in the canonicalized Sum
    (canonical sort key changed under the new default).
  • test_tensor_network: index canonical order for the two G/T pairs swaps
    in the V3 backend (V1 unchanged).

Validation

All SeQuant C++ unit tests pass locally. Pre-existing failures unrelated
to this PR: btas/unit/*, tiledarray/unit/*, sequant/unit/python/basic/run.

Verified downstream in mpqc4 PNO-CCSD eval trace on c4h10/cc-pVDZ: the
previously-distinct half-PNO-transformed g(μ̃,μ̃,Κ) · C(μ̃; a^{ij})
intermediates now share a single cache key (180…8566) instead of two
separate ones, halving the materialized tile memory from 3.32 GB to 1.66 GB.

@evaleev evaleev force-pushed the evaleev/feature/tensor-bks-from-field-and-hermiticity branch 2 times, most recently from 5451e14 to f6b9e33 Compare June 3, 2026 03:25
evaleev added 5 commits June 2, 2026 21:17
Tensor::adjoint() relabels a BraKetSymmetry::Nonsymm tensor's label with
U+207A ('⁺'). Until now those marked leaves bubbled all the way to leaf
evaluators, forcing each backend's evaluator to recognise the marker,
strip it, swap bra/ket, and conjugate by hand — driving e.g. mpqc4's
cck_so_response.ipp to extend is_pert_tensor / eval_so_response_tensor
with adjoint-aware lookups, and pushing H_bar_f12 / eom_cck to force
Field::Complex just to keep the legacy Conjugate path alive.

This adds a first-class EvalOp::Adjoint IR node so the eval tree itself
carries the unary semantics:

  Adjoint(Tensor{<bare>}, Constant{1})

The Constant(1) right child is a sentinel — present so the
FullBinaryNode invariant (every non-leaf has both children) holds
without introducing a true unary node and reworking the iterator. The
evaluate dispatcher only walks the left operand for Adjoint nodes;
linearize_eval_node and to_expr round-trip back to the marker-bearing
tensor stored in the Adjoint node's ExprPtr.

binarize(Tensor const&) intercepts leaves whose label ends in the
adjoint marker, builds the bare-label operand via Tensor::adjoint()
(toggles marker off + swaps bra/ket back), and wraps it in an Adjoint
EvalExpr whose canon_indices match the t⁺ slot order — so a Sum/Product
parent sees the operand at the slot order it expects.

Result::adjoint(ann) is a new virtual on the Result base, defaulting to
throw (mirrors slice_mode's precedent). ResultTensorTA implements it as
a permutation + elementwise conj guarded by is_complex_v<numeric_type>
(elided for real). ResultTensorOfTensorTA implements the real case;
complex ToT throws because TA lacks a conj overload for the inner
Tensor<Tensor<complex>>.

A TDD section 'Adjoint op' in test_eval_expr pins the IR shape:
- op_type() == EvalOp::Adjoint
- left = bare leaf with bra/ket swapped and marker stripped
- right = Constant(1) sentinel
- canon_indices match t⁺
- hash distinct from bare leaf
- Conjugate/Symm tensors don't trigger the op (no marker added)

All 59 unit test cases pass (6725 assertions).
… review)

- export: emit the bare-leaf operand (node.left()) for EvalOp::Adjoint
  instead of node->expr(); the latter equals the assignment target
  (node->as_tensor()), producing a self-referential result=result that
  never reads the operand. The operand's swapped bra/ket order expresses
  the transpose.
- Memory: count both the bare operand and the adjoint result as live at
  peak (the backend materializes a fresh array), not a single tensor.
…itian, not Context

Legacy Tensor ctors that take BraKetSymmetry defaulted bks to
get_default_context().braket_symmetry() — a property of the active
Context, not of the tensor itself. That made the same tensor expression
get different default braket_symmetry_ values depending on what context
the call site happened to inherit (e.g. complex defaults from
mpqc::load_convention), with no relation to the tensor's actual base
field.

A tensor's BraKetSymmetry should instead be deduced from its own
spaces' field and its Hermiticity — that's what the field-agnostic
ctor family (Tensor(label, bra, ket, Symmetry, Hermiticity, …)) already
does via to_braket_symmetry(h, base_field(bra_, ket_)).

Change: replace the Context-defaulted bks parameter with
std::optional<BraKetSymmetry> bks_opt (default std::nullopt) on the
6 legacy ctors (2 primary reserved_tag + 4 public delegators). In the
primary ctor's init list, resolve bks_opt.value_or(
to_braket_symmetry(Hermiticity::Hermitian, base_field(bra_, ket_))).
Member declaration order guarantees bra_/ket_ are constructed before
braket_symmetry_, so reading them in the init expression is well-defined.

Callers that pass an explicit BraKetSymmetry (e.g. BraKetSymmetry::Nonsymm
for amplitudes) implicitly construct std::optional<BraKetSymmetry> and
behave as before. Callers relying on the old Context-derived default
(C tensors in csv_transform_impl, overlap factories, etc.) now derive
from the indices' fields directly:
- real field → Symm
- complex field → Conjugate

Validated on mpqc4 PNO-CCSD eval trace: previously the half-transformed
intermediate g(μ̃μ̃,Κ) * C(μ̃; a^{ij}) materialized under two cache keys
(separate 1.66 GB tiles, one per a_1<i,j> vs a_4<i,j>); now it shares a
single key — 1.66 GB instead of 3.32 GB.
…ensor property

Following the previous commit that taught the legacy Tensor ctors to
derive their default BraKetSymmetry from the indices' field + Hermitian,
Context::braket_symmetry() is no longer the source of truth for any
Tensor. A tensor's bra<->ket exchange symmetry is its own physical fact
(field-resolved Hermiticity), not a property of the active Context.

Removals:
- Context::braket_symmetry() accessor and braket_symmetry_ member
- Context::Options::braket_symmetry field
- Context::Defaults::braket_symmetry constant
- Context::set(BraKetSymmetry) setter
- braket_symmetry() comparison from Context::operator==

Updates to the (now) only remaining users of the old default:
- v1/deserialize.cpp::to_default_symms: the BraKetSymmetry fallback used
  when a serialized tensor omits its braket_symmetry is now a literal
  Conjugate (matching the historical Context::braket_symmetry() default).
  The v1 serializer always emits braket_symmetry explicitly, so this
  fallback only kicks in for legacy/short input.
- v3.cpp dummy-edge strict-assert: the per-edge upper bound on bra/ket
  endpoints can no longer be selected by a single Context value (per-tensor
  BraKetSymmetry would require per-vertex resolution). Use the looser
  (nbra + nket <= 2) bound that subsumes both old branches; the per-tensor
  refinement is left as a follow-up TODO.

Tests:
- test_runtime: drop the .braket_symmetry option and the post-reset
  Conjugate check.
- test_parse: a deserialized ResultExpr with serialized-form omitting
  braket_symmetry now compares to BraKetSymmetry::Conjugate literally
  (the v1 fallback), not to the Context-driven default that no longer
  exists.
…nonical-form snapshots

Followups to the Context::braket_symmetry removal: the deserializer's
BraKet fallback should match the programmatic Tensor default (derive
from base_field + Hermitian when nothing else pins it) — otherwise the
same expression built two ways (ex<Tensor>(label, bra, ket) vs
deserialize("foo{bra;ket}")) ends up with two different
braket_symmetry_ values and the two ResultExprs compare unequal.

Change:
- v1/ast_conversions.hpp: DefaultSymmetries' BraKet slot becomes
  std::optional<BraKetSymmetry>. std::nullopt means "defer to the
  Tensor ctor's derive-from-indices+Hermitian default". to_symmetries
  returns the optional through to the ex<Tensor> call, which the new
  legacy ctor handles via bks_opt.value_or(to_braket_symmetry(
  Hermitian, base_field(bra_, ket_))).
- v1/deserialize.cpp: to_default_symms now initializes BraKet to
  std::nullopt instead of Conjugate. Callers who want a fixed fallback
  (e.g. optimize CSE tests using BraKetSymmetry::Nonsymm for amplitudes)
  pass options.def_braket_symm explicitly and that wins, unchanged.

Test snapshot rebaselines:
- test_mbpt: C{;;x_1,x_2} now spelled :N-S-S — empty bra/ket =>
  base_field Real => Symm under the new default.
- test_wick: the two -1/2 terms swap order in the canonicalized Sum
  (canonical sort key changed under the new default).
- test_tensor_network: index canonical order for the two G/T pairs
  swaps in the V3 backend (V1 unchanged).
- (Previously folded into the Context removal commit: test_canonicalize,
  test_main, test_runtime, benchmarks/main.cpp dropped braket_symmetry
  option lines / set(BraKetSymmetry::Symm) chain links.)
@evaleev evaleev force-pushed the evaleev/feature/tensor-bks-from-field-and-hermiticity branch 6 times, most recently from 7579ce3 to fb54495 Compare June 3, 2026 07:50
@evaleev evaleev force-pushed the evaleev/feature/tensor-bks-from-field-and-hermiticity branch from fb54495 to 9f15f56 Compare June 3, 2026 14:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR redefines bra↔ket exchange symmetry as an intrinsic per-tensor property (derived from indices’ scalar field + Hermiticity) rather than a Context setting, removing Context::braket_symmetry and updating construction/serialization/test behavior accordingly. It also introduces an explicit EvalOp::Adjoint IR node to represent adjointed Nonsymm tensors (label-marked with ) as a unary evaluation operation instead of a leaf-label artifact.

Changes:

  • Remove Context::braket_symmetry (options/defaults/setter/equality) and update tests/benchmarks that previously configured it.
  • Update legacy Tensor constructors to accept std::optional<BraKetSymmetry> and derive the default from base_field(bra, ket) + Hermiticity::Hermitian (with a special-case for empty bra/ket).
  • Add EvalOp::Adjoint to the eval IR, including binarization, evaluation dispatch, export handling, and TiledArray backend support.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/unit/test_wick.cpp Rebases expectations and adjusts context setup by forcing index spaces to Field::Real rather than setting braket symmetry on Context.
tests/unit/test_runtime.cpp Removes braket-symmetry context option assertions and setters from context runtime tests.
tests/unit/test_parse.cpp Updates expectations for deserialization fallback braket symmetry (now asserted as Conjugate).
tests/unit/test_main.cpp Drops braket symmetry from default context initialization.
tests/unit/test_eval_expr.cpp Adds coverage for representing adjointed Nonsymm tensors as EvalOp::Adjoint nodes during binarization.
tests/unit/test_canonicalize.cpp Removes reliance on Context braket symmetry for canonicalization tests; uses explicit per-tensor symmetry instead.
SeQuant/core/io/serialization/v1/deserialize.cpp Changes default-symmetry fallback logic now that Context::braket_symmetry is removed.
SeQuant/core/io/serialization/v1/ast_conversions.hpp Refines braket symmetry parsing to preserve concrete v1 encodings and introduces new Hermiticity spellings (H/A).
SeQuant/core/expressions/tensor.hpp Updates legacy ctor defaults to derive BraKetSymmetry from indices’ field + Hermiticity instead of Context.
SeQuant/core/export/export.hpp Adds export-side handling for EvalOp::Adjoint nodes.
SeQuant/core/eval/result.hpp Extends Result with a virtual adjoint() op used by the new eval IR node.
SeQuant/core/eval/eval.hpp Adds EvalOp::Adjoint evaluation dispatch; updates logging/bytes accounting for unary nodes.
SeQuant/core/eval/eval_node.hpp Updates linearization and cost models for unary adjoint nodes.
SeQuant/core/eval/eval_expr.hpp Extends EvalOp enum and EvalExpr API to recognize adjoint nodes.
SeQuant/core/eval/eval_expr.cpp Teaches binarize(Tensor) to translate label-marked adjoints into EvalOp::Adjoint nodes with a sentinel right child.
SeQuant/core/eval/backends/tiledarray/result.hpp Implements Result::adjoint() for TiledArray tensor and tensor-of-tensors results.
SeQuant/core/context.hpp Removes Context braket symmetry defaults/options/accessors/setters.
SeQuant/core/context.cpp Removes braket symmetry from equality and context construction/setters.
benchmarks/main.cpp Drops braket symmetry from benchmark context setup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +286 to +289
// braket_symm is now std::optional<BraKetSymmetry>: nullopt means
// "neither the serialized form nor the deserialization options pinned
// a value, so let the Tensor ctor derive from base_field + Hermitian"
// (matches the programmatic default of ex<Tensor>(label, bra, ket)).
Comment on lines +279 to +293
///
/// \brief Take the adjoint (complex-conjugate transpose) of this result.
///
/// Used to evaluate the EvalOp::Adjoint IR node — the unary op that holds a
/// bare-label operand and emits T† = conj(T) permuted into the adjoint slot
/// order. \p ann is [operand_annot, result_annot] (bra/ket swapped relative
/// to the operand); backends with a real numeric type implement this as a
/// pure permutation (conj is a no-op) and complex backends apply conj as
/// well. Not a pure virtual: only tensor-backed results need it; the
/// default throws. Mirrors the slice_mode precedent.
///
[[nodiscard]] virtual ResultPtr adjoint(
std::array<std::any, 2> const& /*ann*/) const {
throw unimplemented_method("adjoint");
}
Comment on lines +201 to +210
case EvalOp::Adjoint:
// Unary IR op. The adjoint result (node->expr(), the marker-bearing
// tensor) is the assignment target node->as_tensor(); the right child
// is the Constant(1) sentinel. Emitting node->expr() here would yield a
// self-referential `result = result` that never reads the operand.
// Instead hand the code generator the bare-leaf operand (node.left()),
// whose bra/ket order is swapped relative to the result — that index
// reordering is exactly the transpose the adjoint must materialize.
expressions.push_back(node.left()->expr());
break;
Comment on lines +256 to +257
transform::DefaultSymmetries symms{
Symmetry::Nonsymm, BraKetSymmetry::Conjugate, ColumnSymmetry::Symm};
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