Nicole 0.4.0 — Entering Beta Stage
Release Date: July 15, 2026
Version 0.4.0 promotes Nicole from Alpha to Beta: the public API is now considered stable, and future changes will follow the compatibility expectations of a Beta-stage project. Alongside this milestone, contract() gains a charge-indexed block-pairing scheme that cuts pairing cost from O(N_A · N_B) to O(N_A + N_B + M), and two minor fixes address a complex-weight display bug and an insecure third-party script in the documentation site. No public API is changed.
🎉 Entering Beta Stage
Nicole's Development Status classifier moves from 3 - Alpha to 4 - Beta. This reflects the maturity of the public API surface after seven Alpha releases (v0.3.0–v0.3.7) covering SU(2) support, device propagation, and index maneuvering, and the project's current test coverage of over 1600 tests. Breaking changes will be called out explicitly and become less frequent going forward.
🚀 Enhancements
Charge-indexed block pairing in contract()
contract() previously discovered compatible block pairs by scanning every (keyA, keyB) pair in the two tensors' data and checking charge conservation and shape compatibility on each. This is O(N_A · N_B · c) in the number of blocks, where c is the number of contracted axes.
The compatibility condition always reduces to qb == group.dual(qa) per contracted axis — this holds for Abelian groups (unique group inverse) and equally for non-Abelian groups, by Schur orthogonality: the neutral channel appears in fuse_channels(qa, qb) iff qb == dual(qa). contract() now exploits this directly:
B.datais indexed once intoindex_B: Dict[Tuple[Charge, ...], list[BlockKey]], keyed by the direction-adjusted charge signature over the contracted axes.- For each block in
A.data, the required partner signature is computed directly from its own charges, and looked up inindex_Binstead of scanning all ofB.data. - The per-pair shape check is kept as a defensive guard against inconsistent ragged-sector bookkeeping, even though charge compatibility now implies matching dimensions by construction.
This turns block-pair discovery into O(N_A + N_B + M), where M is the number of actually compatible pairs, with the largest gains on tensors with many distinct charge sectors per axis. The zero-contracted-axes (outer product) case falls out of the same scheme without special-casing, since every block then shares the empty signature.
_dir_weight is simplified alongside this change to return only the orientation-adjusted charge, since the symmetry group is now read directly from the Index at each call site rather than threaded through the return value.
🛠️ Minor Fixes
Complex-weight sign indicator in tensor summaries
tensor_summary previously rendered the sign suffix for an SU(2) intertwiner weight as {+} or {-} based on a >= 0 comparison, which raises TypeError on complex-valued weights. Complex weights are now shown with a dedicated {✕} suffix instead of being compared for sign.
Removed insecure polyfill script from documentation site
The polyfill.io script referenced in mkdocs.yml for ES6 polyfills has been removed. The domain has changed ownership since the script was added and has been used to serve malicious code to sites that still reference it; MathJax rendering is unaffected, as polyfill.io was not required for it.
🧪 Test Suite (1632 tests)
- 1622 tests pass, 10 skipped (CUDA-only tests on a CUDA-less CI runner)
- 7 new tests in
tests/operations/test_contract.pycovering many-sector contraction forU1Group,ProductGroup, and SU(2) against brute-force manual references, multi-axis contraction with 2–3 contracted pairs, the zero-contracted-axes outer-product case, and a sparse many-block case with mostly incompatible sectors
📊 Statistics
Code Changes
- 7 commits since v0.3.7
- 6 files changed: 312 insertions, 38 deletions
- Source modules touched:
src/nicole/contract.py,src/nicole/display.py - Test module touched:
tests/operations/test_contract.py - Documentation touched:
mkdocs.yml,docs/getting-started/changelog.md
✅ Compatibility
Breaking Changes: None. All previously valid calls continue to work unchanged; the contraction result is bit-for-bit identical to the previous implementation, only the block-pair discovery algorithm changed.
Requirements:
- Python ≥ 3.11
- PyTorch ≥ 2.5
- Yuzuha ≥ 0.1.5
📝 Notes
The charge-indexed lookup relies on group.dual correctly implementing "the representation that makes contraction well-defined," per its existing contract in src/nicole/symmetry/base.py — this is not a new assumption, just the first place in the codebase that exploits it to avoid an exhaustive scan. trace() is unaffected by this change: its block loop is already O(N) over a single tensor's data, not a pairwise scan between two tensors.