core: BraKetSymmetry of a Tensor deduced from indices' field + Hermiticity; drop Context::braket_symmetry#539
Open
evaleev wants to merge 8 commits into
Open
Conversation
5451e14 to
f6b9e33
Compare
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.)
7579ce3 to
fb54495
Compare
… + Hermitian, not Context
fb54495 to
9f15f56
Compare
… is a tensor property
…res spaces_ via shared_ptr
Contributor
There was a problem hiding this comment.
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
Tensorconstructors to acceptstd::optional<BraKetSymmetry>and derive the default frombase_field(bra, ket)+Hermiticity::Hermitian(with a special-case for empty bra/ket). - Add
EvalOp::Adjointto 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}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 nota property of the active
Context. This PR removesContext::braket_symmetryand teaches the legacy
Tensorctors to derive theirBraKetSymmetrydefaultthe same way the field-agnostic
Hermiticity-taking ctors already do.What changes
1.
Tensorctors: defaultbksdeduced from indices, not Context (86a613b3c)The 6 legacy
Tensorctors (the two primaryreserved_tagones + 4 publicdelegators) previously defaulted
BraKetSymmetry bkstoget_default_context().braket_symmetry(). They now takestd::optional<BraKetSymmetry> bks_opt = std::nulloptand 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 beforebraket_symmetry_, so reading them in the init expression is well-defined.Callers that pass an explicit
BraKetSymmetry(e.g.BraKetSymmetry::Nonsymmfor 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 directlyfrom the indices' fields:
SymmConjugate2. Remove
Context::braket_symmetry(69b11265c)Removals:
Context::braket_symmetry()accessor andbraket_symmetry_memberContext::Options::braket_symmetryfieldContext::Defaults::braket_symmetryconstantContext::set(BraKetSymmetry)setterbraket_symmetry()comparison inContext::operator==Updates to the remaining users:
v1/deserialize.cpp::to_default_symms: fallback initialized toBraKetSymmetry::Conjugate(later relaxed — see commit 3).v3.cppdummy-edge strict-assert: the per-edge upper bound on bra/ketendpoints can no longer be selected by a single Context value (per-tensor
BraKetSymmetrywould require per-vertex resolution); uses the loosernbra + nket <= 2bound that subsumes both old branches.braket_symmetryoption /set(BraKetSymmetry::Symm)chainlinks 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 matchthe programmatic
ex<Tensor>(label, bra, ket)default — otherwise the sameexpression 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 nowstd::optional<BraKetSymmetry>.std::nulloptmeans "defer to theTensor ctor's derive-from-indices+Hermitian default".
to_symmetriesreturns the optional through to the
ex<Tensor>call.v1/deserialize.cpp:to_default_symmsinitializes BraKet tostd::nullopt. Callers who want a fixed fallback (e.g. CSE tests usingBraKetSymmetry::Nonsymmfor amplitudes) still setoptions.def_braket_symmexplicitly and that wins, unchanged.Test snapshot rebaselines:
test_mbpt:C{;;x_1,x_2}now spelled:N-S-S— empty bra/ket ⇒ Realfield ⇒ Symm under the new default.
test_wick: the two-1/2terms 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 swapsin 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.