mbpt: closed-shell Kramers (time-reversal) CC tracer (round 2) - #566
Draft
kshitij-05 wants to merge 90 commits into
Draft
mbpt: closed-shell Kramers (time-reversal) CC tracer (round 2)#566kshitij-05 wants to merge 90 commits into
kshitij-05 wants to merge 90 commits into
Conversation
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
marked this pull request as draft
June 30, 2026 16:28
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.
…feature/kramers-tracing-round2
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.
…mers-tracing-round2
…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).
…ture/kramers-tracing-round2 # Conflicts: # SeQuant/core/eval/eval_expr.cpp # SeQuant/core/eval/eval_expr.hpp # SeQuant/core/tensor_network/v3.cpp
… ToT adjoint end-to-end TA test The leaf-yielder test now parses its tensor NonHermitian (a Conjugate leaf in swapped orientation binarizes to an Adjoint node, not a leaf). ta_tot_adjoint_end_to_end drives Result::adjoint() with data through the Adjoint IR node against a yielder serving only the canonical orientation (NB not yet compiled here: no TA-enabled SeQuant build config exists on this machine).
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.
…ture/kramers-tracing-round2
Traces the CSV-proto'd MP1 energy with fold_T=false and requires no summand to carry a repeated index inside one bundle; also exercises the swap_spin conjugate-pair fold that crashed the h2o PNS-MP1 derive.
First-ever compile of these TUs (TA-enabled test build now works against mpqc4's release _deps): the deserializer's Conjugate fallback let the always-on braket fold treat unrelated random test tensors as Hermitian partners (yielder key misses, changed tree shapes). Declare the data's true symmetry, matching the symbolic-suite policy. The cache_manager_batch_axis_veto keep-predicate section still fails with honest declarations — batching-era logic, never compiled before, likely pre-existing.
A marker-conjugated (folded) tensor spells conj(bra<->ket-swapped); csv_transform / density_fit / tensor_hypercontract rebuilt tensors from the raw slot layout and silently dropped the conjugation, producing value-wrong factorizations of folded inputs. Normalize each rule's input tensor to the value orientation at entry; value_oriented moves from eval_expr.cpp's file scope to core/expressions/tensor.hpp.
…ture/kramers-tracing-round2 # Conflicts: # SeQuant/domain/mbpt/rules/csv.cpp
expand_antisymm's raw-permutation rebuild and swap_spin's flavor relabeling are slot-preserving but reconstructed tensors from parts, silently dropping Tensor::conjugated(): a folded g^* expanded into raw NonSymm leaves lost its conjugation (value-wrong Kramers-traced energy in downstream consumers). Copy the marker on every slot-preserving rebuild.
…ture/kramers-tracing-round2
…reProtoIndices)" This reverts commit 5fecd61.
Delete TensorBlockCanonicalizer::fold_conjugate_braket and orient_braket_by_color (zero callers; apply_canonical_braket_orientation is the live fold) and repoint the comments that named them. Document SlotCanonicalizationMetadata::conj as the swap parity with its single-consumer invariant, and give its detection the same c-number guard as create_graph. Assert the real-field no-marker precondition at the three spin.cpp rebuild sites that drop the conjugation marker (swap_bra_ket, remove_spin, merge_tensors). Retire stale exploit_conjugate test tags and fix comment typos.
First-ever compile of these TUs (TA-enabled test build now works against mpqc4's release _deps): the deserializer's Conjugate fallback let the always-on braket fold treat unrelated random test tensors as Hermitian partners (yielder key misses, changed tree shapes). Declare the data's true symmetry, matching the symbolic-suite policy. The cache_manager_batch_axis_veto keep-predicate section still fails with honest declarations — batching-era logic, never compiled before, likely pre-existing.
…ies the marker The strict braket sanity check on dummy edges predates the canonical braket-orientation fold: it only knew BraKetSymmetry::Symm as orientation-free, so a foldable Conjugate tensor spelled in the swapped orientation made a legal bra-bra/ket-ket contraction edge trip the assert (every Debug/assert-enabled CI job died there). The check now uses the fold's own predicate: Symm, or Conjugate and c-number and not pinned. Also remove_spin now carries the elementwise-conjugation marker through its relabeling rebuild (relabeling commutes with conjugation) -- the naive-V1 spintrace path reaches it with folded tensors, which the new no-marker assert caught.
Declare orientation-rigid tensors (amplitudes, DF factors, residual heads) braket-Nonsymm in the eval/btas/tapp/extract_subtrees tests and in the cost_analysis / external-interface example inputs, so the canonical braket-orientation fold does not reorient them: tree-shape predicates, yielder cache keys, and the cost_analysis reference outputs all stay put (cost_analysis references are unchanged). Port the ToT adjoint end-to-end TA test. The ITF references are regenerated: the canonicalization rework relabels CSE intermediates (value-preserving, a few more intermediates than before).
…tion (#59) Within a term, leaves connected by shared Kramers-flavored slot indices form flip components; a fully internal component (no external and no dangling flavored index) is reoriented by global time reversal when its flipped fingerprint is lexicographically smaller: every index flips flavor and every leaf takes the elementwise-conjugation marker. The TRS phases cancel pairwise on the component's contracted lines, so no scalar arises (same reason the tracer's closed-contraction T-fold is sign-free). Proto bundles follow their flipped referents through the replacement map; decoration alone never creates connectivity. Frozen components (touching externals) keep their orientation -- their blocks remain served by the existing emission-level TRS derivation.
In-place flavor flips replay reserved tmp ordinals (the make_index_with_spincase ctor throw on canonicalizer-minted dummies) and can collide with same-ordinal dummies of the flipped space, so map every flipped index to a fresh Index::make_tmp_index of the flipped space instead (composites via the proto-carrying overload, protos mapped through the plain entries).
create_graph no longer rejects a dummy connecting bra-to-bra through an adjoint braket-Conjugate c-number tensor: the orientation fold may spell such a tensor bra<->ket swapped, so the connection is legal. Pin that with REQUIRE_NOTHROW and keep the covariance check exercised through a braket-Nonsymm pair, which cannot be reoriented and must still throw.
…7bb3ec) into kshitij/feature/kramers-tracing-round2 # Conflicts: # tests/unit/test_canonicalize.cpp # tests/unit/test_eval_expr.cpp
Flips never cross a Sum boundary: a nested Sum's flavored indices are frozen for the outer analysis, and the outer scope's indices plus the term externals are frozen for the nested analysis.
Removes the kram-stage progress prints, the proto-repeat-bundle report, and the per-block pre-canon LaTeX dump; all were env-gated debugging aids for issues since resolved.
…g in AdditiveModel flops_counter returns numeric_limits<double>::max() for contractions the tensor-of-tensor evaluator cannot perform (a bare index summed between two proto-bearing operands, or a proto-value index summed). AdditiveModel::relax scaled and added that sentinel like a regular cost: with a volatile weight w > 1 (w * max) or two unevaluable children (max + max) the candidate cost overflowed to +inf, which the <= acceptance test (init = max) never accepts, so a subset could end with lp = rp = 0, reconstruct emitted an empty child sequence, and single_term_opt read past the end of an empty small_vector (segfault at address 0x10 on the Kramers-restricted CSV-CC residual, MPQC volatile_weight = 10). Keep the sentinel saturating: an unevaluable split costs exactly max regardless of weight and children, so it stays acceptable and is chosen only when no finite split exists; the CSE meta cost gets the same treatment.
…e contractions The both-ToT guard costed every contraction of a bare shared index between two proto-bearing operands as unevaluable. The re-nesting of a flat carrier into a pair's CSV basis, Z(ij,nu;a<ij>) . C(ij,nu;b<ij>), is that pattern and evaluates exactly (it is how the CSV driver g.C.C has always been evaluated: both operands share their outer pair axes, the bare index is an ordinary mode). Only the inter-pair overlap C(ij,mu;a<ij>) . C(kl,mu;a'<kl>) -- different proto bundles -- cannot be expressed by the ToT einsum. With the old guard the only finite route for a residual term with two external CSV indices was the C (x) C outer product, which outer-product pruning removes as disconnected, so the DP was forced through DBL_MAX routes that evaluate silently wrong; disabling the pruning instead materializes the mu^2 intermediate (OOM on a localized 224-virtual case). Return DBL_MAX only when the operands carry different proto bundles.
In a multi-tensor network the elementwise-conjugation marker (Tensor::conjugated()) is the only carrier of conjugation once each BraKetSymmetry::Conjugate tensor's orientation has been canonicalized, but the vertex painter did not color it: C(x;m) C*(y;m) and C*(x;m) C(y;m) were one colored graph with an automorphism exchanging the two factors, bliss pinned the canonical order of the named indices x, y by their labels alone, and the two spellings (S and S^T*, the same only for real C) shared one eval-node hash and one connectivity graph with inconsistent layouts. In the Kramers-restricted CSV coupled-cluster residual the inter-pair PNS overlap is exactly this network, and the eval cache served S where S^T* was needed. Add CreateGraphOptions::color_conjugation (VertexPainterImpl third parameter), off by default, that perturbs the core-vertex color of a conjugated tensor, and turn it on in canonicalize_slots: that is the value-identity canonicalization (eval-node hash, connectivity graph, canonical slot order), where every tensor is already in its canonical orientation. canonicalize() keeps the marker-blind coloring since it toggles the marker while re-orienting Conjugate tensors (a marker-dependent coloring is not a fixed point there). Only marked tensors change color, so marker-free networks keep their canonical orders. Unit test: eval_expr_conjugation_marker_identity (the two spellings share one hash and agree on which slot is the unconjugated factor's).
…and alignment The tensor an eval node spells for its value (expr()) is what an enclosing tensor network sees when the node is an opaque factor; a Sum copies its first summand's tensor as that placeholder. detail::make_tensor sorted the placeholder's slots by label, so two relabeled spellings of one sum, whose values are transposes of each other, spelled one identical placeholder and an enclosing product got one eval-node hash and one canonical layout for both: in the Kramers-restricted CSV coupled-cluster vv-ladder, the 16-flavor sum of C*C*gCC transforms and its a1<->a2 copy contracted with t, served from one cache slot without the transposition. binarize(Sum) now spells the placeholder's slots (within each group) in the node's canonical index order, the order the value is laid out in (make_tensor gains keep_order for that caller; every other intermediate is still label-sorted, so contraction spellings and exported code for products are unchanged), and folds each summand's alignment to the sum's frame (the first summand's canonical layout) into the Sum hash, so sums whose summands are permuted inconsistently no longer share a key while relabelings of one sum still do. The export fixtures for sums are regenerated for the new placeholder spelling (the generated code is unchanged up to the index order of sum intermediates; batched_sum_inary_contract's ITF section keeps its pre-existing tabs, hence the bypassed no-tabs hook). Unit test: eval_expr_opaque_factor_layout.
…batch axes A Sum factor is opaque to the single-term optimizer (opt_mixed_product prices it as a placeholder tensor and puts the Sum back untouched), so the contraction nodes inside it are not DP nodes and have no entry in BinarizationOptions::node_batch_axes. binarize(Product) nevertheless walked into the Sum with the shared post-order counter, so every outer node's annotation landed on the wrong (inner) node: a contracted-axis batch mark on a node whose result still carries that axis, which the batched evaluator then tried to accumulate across slices (TA_ASSERT left.trange() == right.trange()). Seen on the nested Kramers-CSV CC residual, whose per-slot flavour sums stay nested. Sum factors are now binarized with a private counter and no axes; their leaves are still sliced on use inside the enclosing batch loop. Test: eval_expr_opaque_sum_factor_batch_annotation.
A contracted-axis batch loop co-evaluated, in the same passes as its trigger, every registered persistent intermediate not yet alive that slices the same axis with the identical partition, and stored all of them eagerly. That materializes every member at once regardless of when the lazy schedule first needs it: on a Kramers-CSV CCD residual one trigger joined 237 finals, 124 of them 0.84 GB each (108 GB of accumulators), while the lazy schedule never holds more than a few of them. The per-term peak budget cannot see this. The new policy flag (default true, the old behaviour) makes the group the trigger alone when false: shared sliced sub-intermediates are recomputed per consumer and each persistent final is materialized when first needed.
…roduct single_term_opt returned any product with fewer than three factors untouched (per-node batch axes cleared), and run_single_term_opt_axes short-circuited nt == 2 with an empty annotation. A two-factor product contracting a batchable index therefore never batched, however far over the peak budget it was -- e.g. the DF driver (Σ g·C)·(Σ g·C) over the aux index, whose Sum factors the mixed-product path stands placeholders in for: every MP1 term in the CSV basis. When per-node axes are requested under a batched objective, a two-tensor product now goes through the DP (one contraction) so the model prices and annotates it. Byte-identical otherwise.
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
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 toevaluate 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 remainingblocks. A consumer (here, MPQC's
CCk/SpinorMP2) evaluates one representativeblock 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:closed_shell_kramers_trace(expr, ext_groups, fold_T, expand_g)2 Re[...].closed_shell_kramers_CC_trace(expr, expand_g, use_T)kramers_config_orbits(n, bit_perms, use_T)kramers_external_generators(rank)kramers_external_blocks(n, antisym_perms, use_T, symm_perms)kramers_config_orbitsbut each member carries its reconstruction transformblock(cfg) = sign·[conj]·permute(block(canonical)).has_antisymmetrizer(expr)RealPart/ImagPart(+real_part/imaginary_part)2 Refold; symbolic-only (no native TN evaluation yet — see below).SeQuant/core/eval/eval_node_compare.hpp— split CSE cache keys bycanon_phase. For complex/Kramers tensors whose leaf value is reconstructeddownstream, 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_blocksreconstruction transforms (antisymmetric residual andraw-g forms). Build glue in
CMakeLists.txt/tests/unit/CMakeLists.txt.Known limitations / follow-ups
BraKetSymmetry::Conjugateis not yet exploited byTensorNetworkV3. Theeval_node_comparecanon_phasesplit is the interim correctness measure; thefaithful 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.
Re()/Im()evaluation of a tensor network — the tracer emitsRealPart/ImagPartmarkers and the consumer either honors them or uses thefold_T=falseform (sum the complex blocks, take the real part).applied here.
Testing
tests/unit/test_spinor.cpp— symbolic structure (orbit counts, fold classes,reconstruction transforms).
(
SpinorMP2/CCk): Kramers-restricted MP2/CCD/CCSD/CCSDT all match thefull-spinor energy to the SCF/precision floor.