refactor(pybind)!: guard UniTensor +/- on matching metadata; drop * / (#934)#997
Conversation
There was a problem hiding this comment.
Code Review
This pull request removes elementwise UniTensor-vs-UniTensor arithmetic operations from the Python surface (raising a TypeError with guidance) as they are not well-defined tensor-network operations, while keeping them in the C++ layer where they are used internally. It also adds comprehensive documentation explaining this decision and unit tests to verify the new behavior. There are no review comments, so I have no feedback to provide.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
(Unitensor + Unitensor) and (UniTensor - UniTensor) are definitely meaningful and essential operations. As are (UniTensor * Scalar) and (UniTensor / Scalar). The non-meaningful operations are (UniTensor +- Scalar) and (UniTensor */ UniTensor) |
|
From #934: The following should be removed or disabled: UniTensor * UniTensor
UniTensor / UniTensor
UniTensor + Scalar
Scalar + UniTensor
UniTensor - Scalar
Scalar - UniTensor
Scalar / UniTensor
UniTensor::Pow
UniTensor::Inv // if element-wise
UniTensor % ...For comparison, these operations do make sense on
I don't have any good suggestion for resolving #753 and #675. Certainly if both tensors have the same labels and in the same order and the bonds match, then preserving them is a good idea. If they have the same labels but in a different order, it should probably be an error; the alternative is some implicit permutation/braiding which should be a deliberate operation. I have no suggestion of what to do if some labels match but others do not. The typical operation will be something like |
Code ReviewImplements the maintainer ruling to remove elementwise What's done well
Findings1. Incomplete removal — ut1 / ut2 # TypeError (removed) ✅
ut1 // ut2 # silently returns linalg::Div(ut1, ut2) — the removed op, via a different dunder ❌
ut1 //= ut2 # silently self.Div_(rhs) ❌This leaves a live bypass for exactly the label-discarding/basis-dependent operation #934 targets, and makes the surface inconsistent ( 2. CI gap — the test suite hasn't demonstrably run on this commit. The visible checks show wheels/formatting/docs green but no 3. Breaking change — needs a changelog/release note. Well-flagged in the title ( 4. Minor (acknowledged): VerdictStrong, well-documented refactor with correct mechanics and great tests — but the removal is incomplete: Posted by Claude Code on behalf of @pcchen |
|
@yingjerkao Please complete the removal. |
|
I am lost for words. A tensor network library that doesn't allow addition of tensors? |
Technical summary: which UniTensor arithmetic is well-defined?Following @ianmccul's points, here is an operation-by-operation summary to help settle the direction, since the disagreement is about the axis of the removal rather than a detail. A
This partition matches #934's own list (remove What this PR does vs. that partition
The PR is inverted on the additive axis: it removes the meaningful The decisive internal contradictionThis PR keeps the C++ On #753/#675 (the label footgun)The real problem those issues raise is that Bottom line#934's text supports keeping I'll revise my review verdict accordingly: this needs a direction decision between the maintainers, not just the Posted by Claude Code on behalf of @pcchen |
|
Here is what I thought now. T1 + T2 is allowed only when bond structure and bond labels and bond ordering are exactly the same. For example, if T1 and T2 are of the same shape, but with different label, T1 + T2 will raise an error. If user is sure that T1 + T2 make sense, then user has to explcitly relabel/permute T1 and T2 so that all bond metadata maches. |
|
Yes, we should keep tensor addition and subtraction when the bonds/quantum numbers match. |
…hon surface (#934) Maintainer ruling (yingjerkao, 2026-07-06, phase2-api-semantics plan T3): UniTensor is a tensor-network object, not a raw array. Elementwise UniTensor(+)UniTensor arithmetic has no general tensor-network meaning -- for BlockUniTensor/BlockFermionicUniTensor it either destroys the block structure or is basis-dependent (#934's enumeration); #753/#675 additionally document it silently discarding labels. Audit (required before any removal): grepped src/linalg/*Ut*.cpp, src/linalg/Lanczos*, and all python pytests/examples for UniTensor elementwise +/-. - src/linalg/Lanczos_Gnd_Ut.cpp (backs cytnx.linalg.Lanczos(method="Gnd"), used by the DMRG examples) uses `new_psi -= alpha*psi_1`, `new_psi -= (alpha*psi_1 + beta*psi_0)`, and `eV += kryVg.at(n)*psi_s.at(n)` as genuine Krylov-subspace axpy. - src/linalg/Lanczos_Exp.cpp (backs cytnx.linalg.Lanczos_Exp, used by the TDVP example) uses UniTensor +/-/+=/-= extensively inside its Lanczos and BiCGSTAB inner solvers (Gram-Schmidt projection, residual updates, etc). - No python pytest or example (DMRG/TDVP/iTEBD/ED) calls `ut1 + ut2` or `ut1 - ut2` directly; they only reach the solvers through cytnx.linalg.Lanczos(...)/Lanczos_Exp(...) entry points. iTEBD's `Hx*TFterm + J*ZZterm` operates on plain Tensor (from physics.pauli/Kron), not UniTensor, and is unaffected. - Conclusion: remove only the python surface; keep the C++ operators (they live in include/linalg.hpp, not include/UniTensor.hpp as initially assumed -- corrected during the audit) because the Krylov solvers genuinely depend on them as vector-space arithmetic. Removed (pybind/unitensor_py.cpp): the UniTensor-vs-UniTensor overload in each of __add__, __iadd__, __sub__, __isub__, __mul__, __imul__, __truediv__, __itruediv__ now raises TypeError via a new raise_unitensor_elementwise_removed() helper. Three named guidance constants (kUniTensorAddSubRemovedGuidance / kUniTensorMulRemovedGuidance / kUniTensorDivRemovedGuidance) cover the eight call sites: the +/- family names Contract()/Kron()/scalar alternatives and points at cytnx.linalg.Lanczos()/Lanczos_Exp()/Arnoldi() for Krylov-style axpy; the * and / messages are remedy-first and name ut.get_block() Tensor-level arithmetic as the escape hatch for genuinely-elementwise use. All scalar<->UniTensor overloads in these same dunder groups (numpy scalars, py::int_, cytnx_double, cytnx_complex128, cytnx::Scalar, both directions, in-place and out-of-place) are untouched and keep working, per the decision record. __radd__/__rsub__/__rmul__/__rtruediv__ never had a UniTensor-vs- UniTensor overload to begin with (Python never reaches them for two UniTensor operands since __add__/etc. handle that case) so nothing changes there. Mod/Pow/Inv (also flagged in #934) are out of scope for this task's literal enumeration and are left untouched. Kept (include/linalg.hpp, include/UniTensor.hpp): the free operator+/-/*/ (UniTensor,UniTensor) overloads and the UniTensor::Add/Sub/Mul/Div(_) member functions (plus the UniTensor_base virtuals) are undecorated (no [[deprecated]] -- the task scope is the python surface only). The canonical rationale lives as a doxygen note on operator+(const UniTensor&, const UniTensor&) in include/linalg.hpp (load-bearing +/- for the Krylov solvers vs. unused-but-not-yet-removed *// Hadamard product/division); every other entry point carries a one-line "internal/advanced ... see operator+ ... for the full rationale" cross-ref. TDD: pytests/binding_unitensor_elementwise_test.py added first (red at base ecb1fbd -- all 4 ops currently succeed instead of raising; green after). Raise-tests pin ordered two-token regexes ((?s)934.*Contract / .*Kron / .*get_block). binding_dtype_test.py's existing UniTensor coverage (test_unitensor_numpy_int32_preserves_dtype et al.) only exercises scalar<->UniTensor arithmetic and needed no changes. Gates: pytest 55 (base) + 16 (new file) = 71 passed, no regressions, including the DMRG/TDVP/iTEBD/ED example suite (8 passed) which exercises the surviving C++ Krylov-solver arithmetic end-to-end. build_py (C++ + pybind) rebuilds clean with zero warnings across the full library and extension module. BREAKING CHANGE (python users): `ut1 + ut2`, `ut1 - ut2`, `ut1 * ut2`, `ut1 / ut2`, and their in-place forms, now raise TypeError when both operands are UniTensor. Use Contract()/Kron() for tensor-network composition, cytnx.linalg.Lanczos()/Lanczos_Exp()/Arnoldi() for Krylov-style vector-space linear combinations, ut.get_block() Tensor arithmetic for genuinely-elementwise block manipulation, or scalar arithmetic (unaffected) for the common "scale by a number" case. Stacked on #991 (fix/pybind-collapse-operator-overloads, tip ecb1fbd); branched as refactor/unitensor-drop-elementwise per the phase2-api-semantics plan's T3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ended) Amends the #934 maintainer ruling: elementwise UniTensor+UniTensor '+' '-' '+=' '-=' are kept on the python surface, but guarded on matching metadata, instead of being removed. '*' '/' '*=' '/=' remain removed (elementwise Hadamard product/quotient of two UniTensors is basis-dependent and has no tensor-network meaning). The four add/sub dunders now check that the two operands describe the same tensor slot -- same uten_type, rank, labels (in order), rowrank, is_diag, and bonds (via unitensor_addsub_metadata_mismatch). On a match they perform the add/sub and preserve the shared labels/name (linalg::Add/Sub reset labels to a plain range and clear the name, which was the original #934 label-discarding complaint); on a mismatch they raise a TypeError naming the specific difference. The C++ operator+/operator- are untouched (the Krylov solvers rely on them). Scalar<->UniTensor arithmetic is unaffected. Tests rewritten: matching-metadata +/-/+=/-= work and preserve labels; mismatched labels/bonds raise TypeError; *//*=//= still raise; scalar paths retained. 20 passed; the 68 non-example pytests still pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0861f01 to
340c8e8
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #997 +/- ##
==========================================
+ Coverage 31.82% 31.94% +0.11%
==========================================
Files 230 230
Lines 33124 33185 +61
Branches 13852 13885 +33
==========================================
+ Hits 10543 10601 +58
+ Misses 15539 15497 -42
- Partials 7042 7087 +45
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 4 files with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
|
Ruling needed: scalar ± UniTensor and scalar / UniTensorThe tensor–tensor axis is now settled (keep guarded
(Note: Technical considerations, for the decision:
Two coherent options:
@yingjerkao — the current PR keeps all five; is that a deliberate scope decision or just out of scope for this pass? Posted by Claude Code on behalf of @pcchen |
|
I thought UniTensor + Scalar throws an exception for Block and Fermionic cases? |
|
Two semantic choices in the metadata guard worth a reviewer's eye — both easy to change if you'd prefer different behavior: 1. Label matching is order-sensitive (strict), no auto-permute. 2. For reference, the full match criteria are: same |
|
We should allow UniTensor/(Rank-0 UniTensor/Tensor) |
Resolution of the open review items — this PR is now approvable as-isMaintainer direction (@pcchen) resolving the two items left open in review: 1. The scalar-operand ruling (my "Ruling needed" table above —
2. The With both items resolved by direction, what this PR delivers stands on its own and matches the agreed rulings: guarded Posted by Claude Code on behalf of @pcchen |
Correction to point 1 above (maintainer ruling, @pcchen)
Updated resolution map for the scalar operands:
The Posted by Claude Code on behalf of @pcchen |
pcchen
left a comment
There was a problem hiding this comment.
Approving. The amended direction is correct and verified: T ± T is kept and guarded on matching metadata (uten_type, rank, labels in order, rowrank, is_diag, per-leg bonds), performing with the shared labels and name restored on match — which directly fixes the #753/#675 label-discarding complaint — and raising a specific, actionable TypeError on mismatch; T * T / T / T (Hadamard) are removed with guidance errors; scalar arithmetic and the C++ solver-facing operator+/- are untouched. Tests pin all of it (matching-metadata perform ×4, mismatch raises, removal raises, scalar survival ×8) and CI is fully green (16 pass).
The remaining semantic questions are resolved by documented maintainer rulings rather than in this PR: UniTensor ± Scalar / Scalar ± UniTensor are ruled not supported (removal is follow-up work mirroring this PR's pattern); tensor-typed scalar division arrives via the #1026 rank-0 follow-up; and /////= will be unbound entirely per the integer-removal direction (#1015 implements this) — none block this merge.
Posted by Claude Code on behalf of @pcchen
Decision
Maintainer ruling (yingjerkao, 2026-07-06, amended) resolving #934, #753, and #675: elementwise
UniTensor ⊙ UniTensorarithmetic splits by whether the operation is well defined.+-+=-=are kept, but guarded. Addition/subtraction of two UniTensors is a meaningful vector-space operation when the operands describe the same tensor slot. The Python dunders now require matching metadata and preserve the shared labels; on a mismatch they raise a guidanceTypeErrorinstead of silently discarding labels.*/*=/=are removed. The elementwise (Hadamard) product/quotient of two UniTensors is basis-dependent and has no tensor-network meaning. Their dunders raiseTypeError.(This amends the earlier ruling, which removed all eight operators from the Python surface.)
What changed
uten_type, rank, labels (in order), rowrank,is_diag, and bonds (unitensor_addsub_metadata_mismatch). On a match they perform the op and restore the shared labels/name —linalg::Add/Subreset labels to a plain range and clear the name, which is exactly the [BUG]UniTensorexposes raw element-wise array operations that are not meaningful tensor-network operations #934 label-discarding complaint. On a mismatch they raise aTypeErrornaming the specific difference and suggestingpermute()/relabel_()to align.TypeErrorwith guidance pointing atContract()/Kron()/ut.get_block().operator+/operator-are untouched — the Krylov solvers (Lanczos_Gnd_Ut.cpp,Lanczos_Exp.cpp) depend on them as vector-space (axpy) operations, and they carry the canonical internal/advanced doxygen note added earlier in this PR.Semantics of "matching metadata"
Order-sensitive: same
uten_type, rank, labels (same order), rowrank,is_diag, and bonds (viaBond::operator==).a["i","j"] + a["j","i"]raises rather than auto-permuting — align the operands first. This keeps the sum/difference unambiguous and label-preserving. (For symmetricBlockUniTensor, the C++Add_already enforces matching bonds/is_diag; the Python guard adds the label check and a uniformTypeError.)BREAKING CHANGE (Python users)
ut1 * ut2/ut1 / ut2(and*=//=) now raiseTypeError.ut1 + ut2/ut1 - ut2(and+=/-=) now raiseTypeErrorwhen the operands' metadata do not match (previously they silently discarded labels). With matching metadata they work and preserve labels.The error messages carry the full migration guidance.
Testing
pytests/binding_unitensor_elementwise_test.py(20 tests): matching-metadata+/-/+=/-=work and preserve labels; mismatched labels/bonds raiseTypeError;*///*=//=raise; scalar paths retained. The non-example pytest suite (68 passed, 1 skipped) and the DMRG dense / TDVP1 / ED Ising examples (4 passed, exercising the surviving C++ solver arithmetic end-to-end) confirm no regression.Stacking
Base PR #991 is now merged; this branch is rebased onto
masterand merges cleanly.🤖 Generated with Claude Code