Skip to content

mbpt: closed-shell Kramers (time-reversal) CC tracer (round 2) - #566

Draft
kshitij-05 wants to merge 249 commits into
masterfrom
kshitij/feature/kramers-tracing-round2
Draft

kshitij-05 wants to merge 249 commits into
masterfrom
kshitij/feature/kramers-tracing-round2

Conversation

@kshitij-05

Copy link
Copy Markdown
Member

Summary

Adds a closed-shell Kramers (time-reversal-symmetry) tracer for relativistic
2-/4-component coupled cluster to mbpt, plus the eval-layer CSE fix needed to
evaluate the complex tensors it produces. This is the "round-2" rebuild of the
Kramers tracer: a small, self-contained API that mirrors the existing spin
tracer in structure but folds Kramers configurations under time reversal instead
of applying Ms conservation.

The tracer takes an all-spinor (Kramers-free) closed-shell expression and emits a
sum over time-reversal-canonical Kramers-block representatives, each with the
{sign, conjugation, permutation} transform that reconstructs the remaining
blocks. A consumer (here, MPQC's CCk / SpinorMP2) evaluates one representative
block per symmetry orbit and fills the rest from the transforms, so the
relativistic residual is computed on a reduced set of unique blocks (doubles:
16 → 5).

All numerics are validated downstream in MPQC against the full-spinor
("spinorbital") oracle: MP2, CCD, CCSD, and CCSDT all reproduce the spinor
energy
(e.g. HSeOH/6-31G X2C: CCD 2.6e-11, frozen CCSDT 2.1e-10). The unit
tests here cover the symbolic structure (orbit counts, fold classes,
reconstruction transforms).

What's added

SeQuant/domain/mbpt/spinor.{hpp,cpp} — the tracer and its orbit machinery:

API Role
closed_shell_kramers_trace(expr, ext_groups, fold_T, expand_g) Energy tracer: 2ⁿ Kramers configs, no Ms filter, global-T (conjugate) pairs folded to 2 Re[...].
closed_shell_kramers_CC_trace(expr, expand_g, use_T) CC residual tracer: A-expands the leading antisymmetrizer Â, folds external Kramers configs under the rank-general external antisymmetry + T. Returns one Kramers-labeled block per external representative.
kramers_config_orbits(n, bit_perms, use_T) Orbits of n-bit configs under bit-permutation generators + global time reversal (the orbit primitive).
kramers_external_generators(rank) The shared external-antisymmetry generator set (Sₖ adjacent transpositions of each external group) — single source used by the tracer, the amplitude allocator, and the leaf reconstruction.
kramers_external_blocks(n, antisym_perms, use_T, symm_perms) Like kramers_config_orbits but each member carries its reconstruction transform block(cfg) = sign·[conj]·permute(block(canonical)).
has_antisymmetrizer(expr) Predicate to route a residual (has Â) vs a fully-contracted energy (no Â).
RealPart / ImagPart (+ real_part / imaginary_part) Symbolic markers for the 2 Re fold; symbolic-only (no native TN evaluation yet — see below).

SeQuant/core/eval/eval_node_compare.hpp — split CSE cache keys by
canon_phase. For complex/Kramers tensors whose leaf value is reconstructed
downstream, two contractions can share a canonical colored graph yet evaluate to
genuinely different (not merely sign-flipped) tensors; without this they
over-merge under CSE and reuse the wrong intermediate. The split only ever
separates cache entries (never merges), so it is correctness-preserving and
real closed-shell paths (all phase +1) are unaffected.

tests/unit/test_spinor.cpp — orbit counts and reconstruction transforms:
MP2-energy fold, rank-general external folds (ranks 1/2/3), CCD R2 → 5 labeled
blocks, internal-fold classes (pp-ladder, ring, separable T2² quad), and the
kramers_external_blocks reconstruction transforms (antisymmetric residual and
raw-g forms). Build glue in CMakeLists.txt / tests/unit/CMakeLists.txt.

Known limitations / follow-ups

  • BraKetSymmetry::Conjugate is not yet exploited by TensorNetworkV3. The
    eval_node_compare canon_phase split is the interim correctness measure; the
    faithful fix is to carry conjugation through canonicalization (TODO noted in
    the header). This is the keystone for the planned next step — teaching the
    canonicalizer time-reversal symmetry so the downstream reconstruction machinery
    dissolves.
  • No native Re()/Im() evaluation of a tensor network — the tracer emits
    RealPart/ImagPart markers and the consumer either honors them or uses the
    fold_T=false form (sum the complex blocks, take the real part).
  • Hermiticity (bra↔ket + conj) folding is deferred; the global-T fold is the one
    applied here.

Testing

  • tests/unit/test_spinor.cpp — symbolic structure (orbit counts, fold classes,
    reconstruction transforms).
  • End-to-end numerical validation lives in the consuming MPQC branch
    (SpinorMP2 / CCk): Kramers-restricted MP2/CCD/CCSD/CCSDT all match the
    full-spinor energy to the SCF/precision floor.

Add spinor.{hpp,cpp}: a Kramers tracer for closed-shell relativistic
(2-/4-component) theories, structured like the naive spin tracer
spintrace_impl but folding whole-config time-reversal (T) partners into
RealPart-wrapped representatives inline. Reuses the Spin quantum number as
the Kramers label (alpha=up, beta=down); expands the integral g while
keeping the amplitude t antisymmetric; leaves particle-interchange (sigma)
to canonicalize. Also adds symbolic RealPart/ImagPart Expr markers.

External-index antisymmetry, hermiticity, and the per-column internal-T-reach
are deferred (CC phase).

test_spinor.cpp: traces the MP2 energy 1/4 g-bar t-bar into 7 canonical
Re[g.t-bar] representatives whose outer coefficients sum to 2^n.
…lic)

Extend spinor.{hpp,cpp} toward Kramers-restricted CCSD (symbolic phase):

- kramers_config_orbits: rank-agnostic n-bit configuration orbits under a set
  of bit-permutation generators + optional global time reversal T (the fold
  engine).
- closed_shell_kramers_CC_trace: stages 1-2 of the CCSD pipeline. Factor out
  the antisymmetrizer  (its bra/ket give the external virt/occ groups), fold
  the external indices (rank-general S_k transpositions + T) into the
  symmetry-unique blocks, and fold the internal contracted indices (enumerate
  + sum, sigma-merge via canonicalize). A-expand, g-expansion and the g TRS
  folds (stages 3-5), and the internal-T-reach, are deferred.
- kramers_external_blocks: per-member sign/conj/perm reconstruction transforms
  (compute the canonical block, fill the rest) -- the eval-time external
  reconstruction (notes' open item).

Validated symbolically (test_spinor.cpp): singles/doubles/triples external
folds (2/5/8 blocks); CCD driver/pp-ladder/ring/quad internal folds
(5 blocks; 3/4/9 internal classes -- the quad's 9-vs-10 noted); reconstruction
transforms (perm/conj<->config invariant, T-pair signs).
Add an optional symm_perms argument (sign +1 permutation generators, default
none) alongside antisym_perms (sign -1), so the eval-time external
reconstruction also covers a raw, non-antisymmetrized g leaf: it folds under
particle interchange sigma (a symmetry, sign +1) + T into 6 blocks, vs the
antisymmetric residual's {P_ab, P_ij} + T into 5. Tested (test_spinor.cpp): the
{sigma} + T case yields 6 blocks with the sigma members carrying sign +1.
Thin public wrapper over the file-local find_antisymmetrizer, so a caller (the
CC spintrace dispatch) can distinguish a residual equation (carries a leading Â)
from a fully contracted scalar (the energy, no Â) and route each to the right
Kramers tracer.
- closed_shell_kramers_CC_trace: fold each residual term over its OWN contracted
  indices, not the whole block over the union of all terms' internals. A term
  invariant under an index (e.g. the driver, which has no internal index) was
  over-counted 2^k. Only surfaces on the full multi-term R2 — the unit tests feed
  single terms, so single-term behavior is unchanged (114 [spinor] assertions
  still pass).
- closed_shell_kramers_trace: add fold_T (default true). With fold_T=false, emit
  every configuration (sigma-merged) verbatim with no RealPart wrapper — the
  complex config sum whose real part the caller takes — for evaluators that
  cannot evaluate Re() of a tensor network (the CCk energy observable).
When expand_g=false, the integral g is kept antisymmetric (ḡ) instead of
expanded to the level-1 raw-g form, so the evaluator fetches the factory [as]
block and the cross-Kramers antisymmetry is handled inside the integral. The CCk
energy path uses expand_g=false; g-expansion (level-1) remains the default and a
later optimization stage. Default expand_g=true preserves SpinorMP2 behavior.
The CCSD Kramers trace now A-expands the antisymmetrizer  (expand_A_op) into
explicit signed external permutations, so the full external antisymmetry —
cross-Kramers pairs included — is explicit per term. The earlier strip-Â +
within-block bitwise form could not antisymmetrize cross-Kramers t-dependent
terms (a post-hoc block sign only flips, it does not antisymmetrize).

Adds two flags: expand_g (expand each ḡ to raw NonSymm leaves so the evaluator
fetches raw ⟨..|..⟩ blocks instead of the factory [as] block, which omits the
cross-Kramers swap for mixed-Kramers pairs) and use_T (fold external configs
under global time reversal; default on).
The eval-node CSE identity (the canonical colored connectivity graph) is value-
invariant for ordinary tensors but not for complex tensors whose leaf value is
reconstructed downstream (e.g. relativistic Kramers t-amplitudes via an external
recon): two contractions can share a canonical graph yet evaluate to genuinely
different (NOT merely sign-flipped) tensors, surfacing as a +1/-1 canon_phase
collision. Keying canon_phase into the eval-node equality splits those apart.
This only ever splits cache entries (never merges), so it is correctness-
preserving; real closed-shell paths (all phase +1) are unaffected and lose no CSE.

Interim hack. TODO: replace with a faithful conjugation-aware eval-node identity
once TensorNetworkV3 exploits BraKetSymmetry::Conjugate (carry the conjugation
through canonicalization, the way canon_phase carries the sign).
compose() composed the reconstruction perm in reversed order
(tx.perm[g_perm[k]] instead of g_perm[tx.perm[k]]) for the convention
block(cfg)[v] = block(canon)[v_perm]. The two orders agree for the abelian
S1/S2 external groups of singles/doubles (all involutions), so CCD/CCSD were
unaffected, but differ for the non-abelian S3+ groups of triples and higher,
where 3-cycles reconstructed to the wrong tensor. Validated offline at ranks
1-4 against a fully antisymmetric + TRS reference (reversed order fails at
rank 3; this order passes to machine precision at every rank).
The S_rank adjacent-transposition generator set for the external Kramers fold
(virtual group [0,rank), occupied group [rank,2*rank)) was hand-built inline in
closed_shell_kramers_CC_trace and, MPQC-side, in two more places (the amplitude
allocator and the leaf reconstruction). Provide it as one shared helper and use
it in the tracer. Pure refactor.
@kshitij-05
kshitij-05 marked this pull request as draft June 30, 2026 16:28
kshitij-05 and others added 19 commits June 30, 2026 17:56
Orphaned when 7f69992 switched A-handling to expand_A_op; its only
references are its own definition and a self-recursive call, so it is
unused. clang -Wunneeded-internal-declaration (-Werror) in SeQuant's
unit build rejects it (the MPQC build that validated the branch uses
looser flags, so it slipped through).
Two eval nodes that share a canonical graph/leaf but differ in
antisymmetric-reorder parity evaluate to negatives of each other
(+T vs -T), so they must not share a CSE cache slot. This was enforced
by a special-cased canon_phase check in TreeNodeEqualityComparator,
flagged as a hack pending a "faithful" fix.

Make canon_phase a first-class part of the node identity: fold it into
EvalExpr::hash_value() (a no-op for real closed-shell paths, where every
phase is +1, so the equality structure is unchanged) and keep the
comparator check as the hash-collision guard. This retires the hack and
fixes silent CSE over-merging of complex/Kramers contractions
(Kramers-restricted CCD now matches the spinor reference).
Add an opt-in exploit_conjugate mode to TensorNetworkV3::canonicalize_slots
(threaded via CreateGraphOptions). When enabled, a BraKetSymmetry::Conjugate
tensor's bra and ket slots are colored identically (as for Symm), so its two
bra<->ket orientations fold onto one canonical graph; the accompanying
conjugation (T{bra;ket} = conj(T{ket;bra})) is recorded as a new
SlotCanonicalizationMetadata::conj byproduct, detected by comparing the
canonical positions of the bra/ket bundle vertices -- the same comparison
canonicalize() uses for its explicit bra<->ket swap.

Default off: Conjugate bra/ket stay distinctly colored (historical behavior),
so all existing canonicalization is byte-unchanged. Groundwork for exploiting
Hermitian (Conjugate) braket symmetry in eval-node CSE (CSV coefficients and,
later, time-reversal folding).
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 the standard (flat)
Kramers CCk t/g -- Conjugate under Field::Complex -- 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
CSV/ToT coefficient's canon_indices out with occupieds first -- the
layout CCk's osv_coeff_layout/pno_coeff_layout detectors rely on. So {}
silently mis-ordered them and CSV-CCk (he2/h2o/he10-csv) aborted with
csv_rank==0 ("Only OSV and PNO coefficients available").

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.
CostProfile (peak/flops/exec) over the factorized IR via a zero-data dry-run
evaluation, driving the batched cost model's predictions.
Perf-first (DenseTimeSpace) objective with peak_threshold as a ceiling;
role-split (contracted/external) batchability; order-aware placement over the
combined nest; per-node batch annotations consumed by the evaluator.
External-mode scatter + contracted accumulate; cache scope chain with
fall-through; slice-on-use; per-level placement driven by a per-canonical
lifetime mask (cross-occurrence meet) unioned with contracted residency;
iterative (stack-safe) tree traversal.
Index-space occupancy predicates robust to non-physical spaces; logger;
is_valid accepts Power; convention.
The orthonormal-basis overlap branch of csv_transform reused
drop_proto_indices() (which keeps the base ordinal, e.g. a↑_1) as the
contracted dummy of the C†C expansion. Two overlaps sharing a base
virtual then collide on the same dummy, so that index appears >2 times in
the term and the eval-time TensorNetworkV3 build rejects it
("Edge::add_vertex: v is already connected"). Take the spin-labeled base
space from drop_proto_indices() (which correctly preserves the ↑/↓ label)
but mint a unique tmp index in it. No effect on the non-relativistic
(comma-only, single-overlap) path.
make_spin{alpha,beta} stamps a whole proto-bundle with the outer index's single Kramers spin, wrong for a CSV/PNS virtual whose proto is the occ pair (each occ carries its own up/down). Rebuild kv.second's proto-bundle from the original config-mapped proto, unconditionally: rebuilding only when some proto-index was in the map missed the all-external-proto case (a down internal virtual of an up external pair in the vv-Fock coupling), whose amplitude then spanned two proto tuples and gained a spurious pair of outer occ axes.
Cost +inf for a bipartition that sums a shared index which a tensor-of-tensor einsum cannot contract, so the DP de-nests against the CSV coefficients first: (1) a non-proto index that is an outer/batch axis of both ToT operands (the bare C.C overlap over the CSV expansion index mu); (2) a proto-value of a ToT operand (the off-diagonal occ Fock summing the amplitude's own pair index). Self-gating: never fires for flat-only networks, nor for f.C/g.C (fresh CSV mode) nor intra-pair vv-Fock (inner virtual).
With kramers=true every spin-labeled expansion dummy (including the
overlap C+C dummy) is summed over both Kramers flavors instead of
inheriting the CSV index's label: per-index-column ms conservation is
correct nonrelativistically but drops the mixed-label C blocks of a
time-reversal-paired spinor basis.
…nicalization

The label-ignoring default (ignore_named_index_labels=Yes) makes same-space
named indices graph-automorphic, so bliss breaks the orbit by input vertex
order: equivalent summands canonicalize to DIFFERENT forms (defeating the
HashingAccumulator merge) and which named label lands on which slot becomes
input-dependent. Canonicalize summands with IgnoreNamedIndexLabel::No —
merging across a sum requires meaningful named labels, exactly as
Sum::canonicalize_impl already does. Adds a CSV-style regression test
(equivalent g*C*C networks under dummy relabeling + factor reorder must
canonicalize identically and merge to 2x one term).
…ymmetries #596) into kshitij/feature/conjugation-symbolic

Master wins wherever it covers the same fix; the branch's extras are ported
onto master's structures:
- tests follow master's expectations where the two sides drifted
  (tensor-network C-entry and index-vector cases, the v3 canonicalization
  hunks, the MBPT L/R starred expectations), with an explicit :N-C-N
  Hermitian entry kept for the conjugation coverage
- Tensor::value_oriented moved out of line into tensor.cpp; the adjoint /
  conjugation-marker composition rule documented on the class and tested
- VertexPainter keeps the conditional colour+conjugation combine (the
  unconditional form was measured to churn eight fixtures)
- utilities/cost_analysis and external-interface examples taken from master
- test_utilities cross-comparison made order-independent
…ymmetries #596) into kshitij/feature/kramers-tracing-round2

Master wins wherever it covers the same fix; the branch's extras are ported
onto master's structures: Re/Im eval nodes, CanonTransform retrieval, lazy
ToT/flat TA views, the Kramers symmetry attribute and partner spaces, the
Kramers-folded leaves, the private-clone summand optimization and the
nested-bracket batch-annotation splice. The result-layout fingerprint is
folded into node ids at the binarize sites (EvalExpr::layout_fingerprint_of)
and in the leaf constructor; a Kramers-folded leaf fingerprints its folded
spelling.

Dropped from the branch: both ToT contractibility guards (single_term_detail
and cost_model now match master; TA's ToT einsum handles pure-external inner
products), the cost-analysis utilities and external-interface examples (taken
from master), and the tests whose expectations had drifted from master's
(tensor-network C entry and index vectors, the v3 canonicalization hunks, the
MBPT starred expectations, the utilities cross-comparison).

Three evaluation defects surfaced by the port, all fixed here:
- fetch_leaf_traced applies the leaf's CanonTransform: the leaf evaluator
  serves the canonical spelling, and master's leaf branch handed it up as if
  it were already oriented (norm-preserving garbage on every Kramers/PNS deck)
- evaluate_antisymm antisymmetrizes over the ket rank of a bra-ket-swapped root
- the product fold composes a node's canonical phase from the flattened
  factors only: the intermediate child's own phase is already inside
  canon.phase, so re-applying it made two spellings of one cache slot differ
  by a sign (cache-on-only errors in spin-orbital CCSD)

Also fixed: optimize_impl re-keyed a Sum after moving it (out_of_range on the
NoReorder path). Env-gated diagnostics added for the last of these:
SEQUANT_EVAL_WARN_CACHE_IDENTITY (re-evaluates every cache hit fresh) and
SEQUANT_CACHE_EXCLUDE_HASHES (runtime slot bisection).

Certified with MPQC: HSeOH PNS-MP1 union_int -0.311350162694, PNS-CCD union_int
-0.328438791800, dense Kramers CCSD h2o -0.135810412687, and master's
spin-orbital CCSD validation input equal to its cache-off value.
…erged) into kshitij/feature/conjugation-eval

Brings master's batched DAG evaluation (#613) and default tensor symmetries
(#596) into the conjugation-eval branch on top of the merged #602.

The eval layer is resolved the way the round2 merge (0843463) resolved the
same conflicts, restricted to what this branch carries: eval.hpp, result.hpp
and the btas/dryrun/tapp/tiledarray result backends, cache_manager.hpp,
ordered_executor.hpp, eval_node{,_compare}.hpp and canon_transform.hpp are
taken from that resolution verbatim; eval_expr.{hpp,cpp} likewise, with the
Kramers-fold leaf paths (kramers_folded_, the flip on retrieval) left out
since this branch has no Kramers partner spaces. That carries the transform
model this branch is about onto master's structures: master's EvalOp::Adjoint
IR node is not emitted -- conjugation and bra-ket swap ride on the leaf's
CanonTransform -- and the three evaluation defects the port surfaced are
fixed here as in round2 (the leaf transform applied in fetch_leaf_traced, the
ket-rank antisymmetrization of a bra-ket-swapped root, the product-fold phase
composed from the flattened factors only).

Tests: master's and this branch's cases are both kept where they conflicted;
the sum-slot identity test follows master's multiset semantics (A + B and
B + A share a slot), the Adjoint-op section asserts the stored canonical
spelling, the export fixtures follow the canonical result layout, and the
optimize() calls that use the deprecated overload are marked as such.
Non-TA suite 271/271; TA-enabled [eval]/[tot]/[view]/[conj-transform] 64/64.
…) into kshitij/feature/kramers-tracing-round2

Round2 already carries every change conjugation-eval made to the conflicted
files (eval_expr, tensor.hpp, optimize.cpp, tensor_network v3 and their
tests), so those resolve to round2's versions; the cost-analysis report keeps
master's text. Taken from the branch: its Re/Im optimize() tests and the
lazy-conj eval design/plan documents.
The shaped-product hook emits the product through TiledArray's expression
layer; for a result with an outer mode of no tiles that layer's general
product has no process grid to lay out. Such a result has nothing to shape:
decline it so the unshaped prod() emits the empty result. Env
SEQUANT_EVAL_WARN_SHAPED_DECLINE=1 reports each decline (annotations and
operand outer tile counts) on stderr.
…x tags the Kramers trace leaves behind

Sum::canonicalize_impl hands its summands to parallel threads
(sequant::for_each) while they routinely share subexpression objects
(tensors reused by expand(), nested Sum factors, a factor used twice in one
product); canonicalizing a shared object from several threads races on the
index tags and memoized labels, which SEQUANT_ASSERT_BEHAVIOR=THROW reports
as Taggable::assign on an already tagged slot. A summand whose tree reaches
an object already reached is now canonicalized on a private clone, as
optimize_impl does.

Index::transform tags every index it replaces; append_spin, kramers_term_flip
and external_swap_image left those tags on the per-block expressions, and the
canonicalizer requires untagged slots. The two relabelings reset their tags
and the Kramers traces reset all tags before each canonicalization.
The peak objectives compared their element-count peak to peak_threshold
(bytes) through a numeric_size nothing ever set (8 bytes), the dry-run
CostModel hard-coded 8 bytes per element, and flops_counter's
multiply-adds were charged as one flop each. On a complex network the
byte budget therefore admitted twice what it named and the modeled flops
were four times too low, so the roofline knobs sat at the wrong
crossover and dry-run predictions were off by constant factors.

Every tensor already carries its field (IndexSpace::field,
Tensor::base_field), so price from it: FieldCostFactors {elem_mult,
flop_factor} = {1, 1} real / {2, 4} complex, and tensors_field() ORs a
network's tensors. roofline_op_cost takes the two factors, with
machine_balance and fast_mem_elems still calibrated per 8-byte element.
PeakModel / PeakBatchedModel contexts carry the factors and report peak
bytes as peak * numeric_size * elem_mult. The dry-run CostModel sizes
each index set by its field, flops() returns real flops, exec_cost()
takes the contraction's field, and meter() accepts the optimizer's
RooflineParams so a replay prices each op the way the DP did.

Tests: byte literals under the default (complex) registry doubled; tests
that model real doubles push scoped_real_field_context()
(catch2_sequant.hpp); new field-factor tests for the factors, the
roofline term and the dry-run sizing.
…nsform hook

Two defects the first metered dry run on the DAG binary exposed, both
silent under an IGNORE assert policy and fatal under THROW:

- is_valid() asserted "Unsupported expression type" on RealPart / ImagPart
  nodes. The time-reversal fold of a complex network emits Re(...) summands
  and the production path validates every summand before binarization, so
  every such run died. Re/Im are valid iff their inner is (they are atomic,
  like Power, so the inner is validated explicitly).

- ResultDryRun / ResultDryRunNested had no apply_transform(): every leaf
  fetch applies its CanonTransform since the transform model, so the meter
  threw "Not implemented" on the first leaf and the whole prediction was
  lost. In the size model the phase and the elementwise conjugation move no
  data; the relabel reorders modes exactly as permute() does.
…n map

optimize_impl optimizes a Sum's summands in parallel (sequant::for_each), and
each summand inserts into, re-keys (Re/Im wrappers, mixed products) and reads
OptimizeOptions::term_batch_axes -- one unordered_map shared by all of them.
Only opt_pure_product's insert was locked; the re-key and mixed-product paths
touched the map unlocked, and an insert racing a rehash on another thread is
a segfault (seen on one rank of an 8-rank run, in _Map_base::operator[]
from optimize_impl). One namespace-scope mutex now guards all four sites; no
locked region calls back into the optimizer, so no re-entrancy.
…ands

index_occurrence_counts recursed through a nested Sum and added up every
summand, so a CSV projection bracket expanded over two or four flavors of
its expansion dummy contributed each external index an even number of times
and the parity-based external set dropped them: every Kramers-split residual
block failed "Inconsistent external indices in sum". A Sum's value fills the
slots of one summand (and is_valid has already checked its summands agree),
so count its first summand only. Test: nested two-flavor bracket beside a
proto-indexed sibling is valid, a genuinely inconsistent sibling still fails.
…in TN and binarize diagnostics

rekey_onto (optimize_impl's Sum branch) found/erased/inserted in the shared
term_batch_axes map without the mutex the other four sites take; for a nested
Sum (a Kramers-split flavor bracket) it runs while sibling summands are still
being optimized in parallel and inserting, so entries went missing at random
and binarize's node_counter == node_batch_axes.size() failed (seen once on the
split PNS-MP1 check, 6/6 passes since). Diagnostics: SEQUANT_BATCH_AXES_DEBUG=1
prints per-summand entry counts on both sides; Product::canonicalize names the
product in a tensor-network exception; the strict bra-ket check names the
dummy and the slots it meets (e.g. i↑_1 on two g bra and two t ket slots after
a Kramers flip collided with an independent dummy).
…ayout fingerprint, block comparison); occurrence-ordered erasure map
… its proto indices; non-leaf factors neutral)
…s past the network's maximum collided with the tmp-index range)
…he union space); refresh the erased identity tensor after the head overwrite; name a failed TA product
…amers_flip on the TA and dry-run backends

kramers_flip_array (backends/tiledarray/kramers_flip.hpp): the time-reversal
flip F over Kramers-union axes; per union mode out[up half] = +conj in[down
half], out[down half] = -conj in[up half] (the sign is that of the target
half; conj applied once over several modes), scaled by a phase; F∘F = -1.
Flat and nested arrays, dense and sparse policies (two block assignments per
mode, no contraction); union_halves_congruent / union_half_tiles check the
concat(tr_up, tr_dn) axis layout.

A KramersFlip node (RealPart pattern: left = the canonical up-partner node,
right = the Constant(1) sentinel) denotes phase * F(left). Identity = the
child's salted hash + op + modes + phase + the flipped layout; the child's
phase/conj channels hoist into the wrapper's transform. make_kramers_flip_node
builds it; EvalMode::KramersFlip, unary_op/apply_one_op, linearize_eval_node
and the exporter know the op. The TA backends flip the materialized logical
array; the dry-run backends return a same-shape cell.
…er (BinarizationOptions::kramers_fold_intermediates)

A Product / Sum whose flavoured externals (after the Kramers-blind erasure of
the pair labels) are down-majority -- a tie resolved by the flavour string in
the flavour-blind canonical order of the externals, so exactly one of two
partners folds -- and whose leaves are all KramersSymmetry::TimeReversal is
binarized as an EvalOp::KramersFlip over its flipped (canonical) spelling:
the partner families share one contraction and differ by an O(size) flip
over the union free legs with phase (-1)^{n_down}. Off by default.
…posites follow their plain slots)

kramers_flipped / kramers_flip_slots leave a composite of an unflavoured
(Kramers-union) space untouched even when its proto indices are flavoured,
so flipping a whole expression with them turned t{a<i↓,j>; i↓, j} into
t{a<i↓,j>; i↑, j} (the amplitude-leaf provider then saw a third outer
index). kramers_flipped_deep / kramers_flip_deep / kramers_flip_slots_deep
flip the protos of such composites too; the fold uses them for the flipped
expression, the uncontracted set, the wrapper's labels and the denoted
tensor.
…um and the shaped-product hook

TA's nested in-place add with a conjugate factor on the added operand
(t += ph * o.conj()) does not conjugate arena inner cells: a KramersFlip
root served through a conj view was accumulated unconjugated (HSeOH
PNS-CCD). ResultTensorOfTensorTA::add_inplace and ::sum now materialize a
conj view (the unary materialization conjugates correctly), as they already
did for an inner relabel. A TA failure inside sum() or the shaped-product
hook now names the operand annotations, as Result::prod's wrapper does.
…es, blind pair labels, conj over no union modes

- collect_tensor_factors: a KramersFlip factor contributes its denoted
  tensor (it was dropped from the parent's network: TA 'right index appears
  in neither the left argument nor the target'); to_expr knows the op.
- a Sum's direct summands never fold as wholes (they are added into the
  Sum's layout), a Product's factors may; a ResultExpr root never folds (its
  spelling is the head's; a folded residual root respelled the block's
  t-dependent intermediates and lost their sharing with the other blocks of
  the same pair).
- the fold's flavoured externals exclude every blind pair label
  (kramers_blind.hpp blind_indices: blind whether or not the erasure rewrites
  it), not only the erasable ones.
- kramers_flip_array over no union modes is the conjugation (times phase),
  not a copy.
…Ops::kramers_flip); the DAG schedule skips a unary node's sentinel

SeQuant's array backends are context-free: what a Kramers-union axis is
(its blocking, tiling and half-swap) is the user's knowledge. The TA
backend's kramers_flip_array / Result::kramers_flip are gone; apply_one_op
realizes an EvalOp::KramersFlip node through the new
BackendArrayOps::kramers_flip closure (installed on the eval cache like the
batching ops; the dry-run ops implement a same-shape copy with no flops).

EvalExpr::is_unary_op names the unary IR ops (RealPart / ImagPart /
KramersFlip); compute_dag_boulevard, the legality walk, the lifetime-mask
walks / value_key_impl and the ordered executor's operand listing no longer
treat the Constant(1) right child of a unary node as an operand (every
sentinel keyed to one value: cyclic step dependencies on an aux-batched
DAG run).
The conj-view materialization added in 2696b66 answered a wrong energy
(HSeOH PNS-CCD, 2026-09-18) that the KramersFlip fold fixes of the same
day (cd746be: conjugation over no union modes, the root never folds)
removed: with the materialization off, every certification deck (HSeOH
PNS-MP1, PNS-CCD, union PNS-CCSD, DAG aux-batched CCD; DCH 6-31G PNS-CCD
with the fold) reproduces its reference, and a TiledArray regression test
(tests/tot_add_conj.cpp: arena and owning inner cells, sparse and dense,
in place and value-returning, null destination cells, real and complex
factors) shows the nested add conjugating every inner cell. Only a
relabeled inner order is materialized, as before that commit.
log_batch_op (Slice / Scatter / Accumulate, now also Clone) was compiled
out unless SEQUANT_EVAL_TRACE was defined, so a wet trace on a release
build showed the per-op Eval lines but none of the slicing, scattering
and partial accumulation between them: on the DCH/v3z PNS-CCSD trace
(2 owl nodes) 53% of an iteration's residual time fell outside every
traced line. The lines are now gated by the trace level alone, like the
Eval lines a Trace::On evaluation emits.
…reduction

log_batch_op summed the moved array's size over the world; a batch op a
rank runs alone would then wait on the others, and every traced Slice /
Scatter / Accumulate / Clone paid a collective.
Every TiledArray backend op ended with DistArray::wait_for_lazy_cleanup,
which blocks until every array scheduled for lazy deletion so far has
finished its pending tasks. On one rank that is a cheap local wait; on
many ranks it drains the pipeline after each of the thousands of ops and
batch slabs an iteration issues, so no op's tail overlaps the next op's
start (864 drains per two-iteration DCH 6-31G PNS-CCD run).

drain_lazy_cleanup routes all 23 sites through one policy flag,
sequant::set_ta_drain_after_op (default true, the historical behavior);
false lets ops pipeline and leaves the cleanup to run alongside the next
op. The SEQUANT_SYNC_STATS counters now also count the zero-array fences
of the batched scatter path.

This branch has not been deployed

No deployments
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