feat(api)!: norm()->Scalar, underscore convention for mutators, unified permute/reshape syntax#1000
Conversation
…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>
Tensor::norm()/UniTensor::norm() and the linalg::norm() free-function
overloads return the 2-norm as a plain double instead of a rank-0
Tensor, per ruling 2 of the API-semantics decision record. The old
Norm()/linalg::Norm() spellings are kept for one release, marked
[[deprecated]] in C++ and emitting a python DeprecationWarning from
their pybind bindings.
All in-tree C++ callers of the old Norm()/linalg::Norm() (Lanczos_Gnd,
Lanczos_Gnd_Ut, Lanczos_Exp, Lanczos_ER, DenseUniTensor::normalize_,
BlockUniTensor::{normalize_,Norm}, BlockFermionicUniTensor::{normalize_,Norm})
are migrated to norm()/linalg::norm() so the build stays
deprecation-warning-clean; the same is done for the three DMRG/TDVP
python example scripts and a pytest assertion that called .Norm()
directly, to keep the pytest run free of deprecation-warning noise.
Adds gtest coverage (Tensor_test.cpp, DenseUniTensor_test.cpp,
linalg_test.cpp) and pytest coverage (pytests/norm_test.py) asserting
norm() returns a float equal to Norm().item() and that both Norm()
spellings (member + linalg free function) warn.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g/combineBond/convert_from (#335/#336/#381/#421/#422) Per ruling 3 of the API-semantics decision record, the `_` suffix now consistently means "in-place, returns self" across the public UniTensor surface; the un-suffixed spelling is either the out-of-place form or, where none existed, a deprecated shim delegating to the new `_` method. - set_name_() / set_label_(): new canonical in-place spellings (both on UniTensor_base and the public UniTensor wrapper); set_name()/set_label() are now [[deprecated]] shims delegating to them (python bindings emit DeprecationWarning). - tag_(): renamed from the virtual tag() across UniTensor_base and the Dense/Block/BlockFermionic overrides; the public tag() is now a deprecated shim. - combineBond_()/combineBond(): per #421/#422, merges the combineBond/ combineBonds duplication (combineBonds was already deprecated in favor of combineBond in earlier work, but combineBond itself still mutated in place with no underscore). combineBond_() is the new in-place form; combineBond() is redefined as the out-of-place form (clone + combineBond_()), completing the underscore convention. combineBonds() remains deprecated as before; its pybind binding now also emits a DeprecationWarning (it previously did not). - convert_from_(): new canonical in-place spelling; convert_from() is now a deprecated shim. set_labels()/relabel(s)_ and set_rowrank()/set_rowrank_() were audited and already follow the convention from earlier work; Bond's mutators are intentionally left untouched here (handled by the parallel refactor/bond-immutable branch). See the audit table in the task report for the full method-by-method verdict. All in-tree C++ callers (Svd/Gesvd/Rsvd/Svd_truncate/Gesvd_truncate/Qr/Qdr call .tag(); Add/Sub/Mul/Div call .set_name(""); DenseUniTensor/ BlockUniTensor/BlockFermionicUniTensor call ->set_label() internally) are migrated to the `_` spellings so the build stays deprecation-warning-clean. The DMRG/iTEBD python examples are migrated for the same reason on the pytest run. Adds gtest coverage (DenseUniTensor_test.cpp, BlockUniTensor_test.cpp) and pytest coverage (pytests/underscore_convention_test.py) asserting each `_` method mutates in place and returns self, and each deprecated spelling still works but warns. The pre-existing combineBond gtest (which asserted in-place mutation under the old contract) is updated to reflect the new combineBond_/combineBond split. BREAKING CHANGE: UniTensor::combineBond(indicators, force) changes from in-place (mutating the receiver and returning *this) to out-of-place (const; returns a new UniTensor with the bonds combined, receiver unchanged) at the same call signature, in both C++ and python. Code that relied on the receiver being mutated by combineBond() must switch to combineBond_(), the canonical in-place spelling. The other renames in this commit are non-breaking (old spellings remain as deprecated shims with unchanged behavior). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pe() (#293) Per ruling 4 of the API-semantics decision record, both call syntaxes are additive on both Tensor and UniTensor: - Tensor.permute()/permute_()/reshape()/reshape_(): previously variadic-only (t.permute(1, 2, 0)); now also accept a single list/tuple argument (t.permute([1, 2, 0])). - UniTensor.reshape()/reshape_(): previously variadic-only; now also accept a list/tuple argument, same as Tensor. - UniTensor.permute()/permute_(): previously list-only, in both int-index and string-label form; now also accept the variadic form for both label kinds (ut.permute(1, 2, 0) and ut.permute("a", "b", "c")). The rowrank keyword argument works identically in all four call forms. Implementation: pybind/pyint_dispatch.hpp gains two small helpers, parse_index_args<T>() and is_string_args(), that unwrap a py::args tuple under either call form (a lone list/tuple argument, or N positional scalars) and, for UniTensor, decide whether the mapper is int- or string-valued. The new py::args-based UniTensor::permute/permute_ overloads are registered after the existing std::vector<...> overloads so existing single-list calls keep matching those directly; only calls with 0, 2+ positional args, or a single non-list/tuple argument fall through to the new catch-all. Tests (pytests/permute_reshape_syntax_test.py): all four call forms (variadic int, list int, variadic label, list label where applicable) on both Tensor and UniTensor, for both permute and reshape, in-place and out-of-place, plus the rowrank kwarg in each form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces several API updates and deprecations, notably replacing the Norm() method (which returns a rank-0 Tensor) with norm() (returning a plain double) across Tensor, UniTensor, and linalg modules. It also establishes the underscore convention for in-place mutators (e.g., set_name_, tag_, combineBond_) and unifies the variadic and list call forms for permute and reshape in the Python bindings. Feedback on these changes highlights a performance regression in DenseUniTensor::normalize_(), where the use of linalg::norm introduces an unnecessary host-device roundtrip on GPU devices; it is recommended to continue using linalg::Norm directly with suppressed deprecation warnings to keep the operation on-device.
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.
| void DenseUniTensor::normalize_() { | ||
| // In-place and dtype-preserving: divide by a rank-0 (shape {1}) Tensor so the | ||
| // division routes through linalg::iDiv, which writes into _block's own storage | ||
| // (an Int32 block stays Int32). Dividing by a bare double or Scalar would take | ||
| // the promoting Div + storage-swap path instead and silently retype integer | ||
| // blocks to Double (regression caught by DenseUniTensorTest.normalize_int_type). | ||
| // The divisor's dtype mirrors what the deprecated linalg::Norm(_block) used to | ||
| // return: Float for Float/ComplexFloat blocks, Double otherwise. | ||
| const unsigned int dt = this->_block.dtype(); | ||
| const unsigned int norm_dt = | ||
| (dt == Type.Float || dt == Type.ComplexFloat) ? Type.Float : Type.Double; | ||
| Tensor nrm({1}, norm_dt, this->_block.device()); | ||
| nrm.at({0}) = linalg::norm(this->_block); | ||
| this->_block /= nrm; | ||
| } |
There was a problem hiding this comment.
The current implementation of normalize_() introduces a significant performance regression on GPU devices. Calling linalg::norm(this->_block) computes the norm on the device, but then copies it to the host (CPU) via .item().real(). Then, nrm.at({0}) = ... copies it back to the GPU. This host-device roundtrip causes unnecessary synchronization and latency.
By suppressing the deprecation warning and continuing to use linalg::Norm(this->_block) directly (which is already done in DenseUniTensor::Norm()), the entire operation is performed on-device without any host-device transfers, preserving optimal performance.
void DenseUniTensor::normalize_() {
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
this->_block /= linalg::Norm(this->_block);
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif
}There was a problem hiding this comment.
Good catch on the GPU roundtrip — fixed in 3ba955c, though slightly differently than suggested to avoid putting the deprecated linalg::Norm back into a new mutator.
normalize_() now just does this->_block /= this->Norm();. UniTensor_base::Norm() is the non-deprecated internal virtual (only the public UniTensor::Norm() wrapper carries [[deprecated]]), and DenseUniTensor::Norm() already delegates to linalg::Norm under a pragma "for one release." So:
- It stays entirely on-device — no
device->host->devicescalar roundtrip/sync. ✅ (your point) - The divisor is a rank-1
{1}Tensor with the exact dtype the old manualnrmmirrored (Float for Float/ComplexFloat, Double otherwise), soTensor /=still routes throughlinalg::iDivs in-place, dtype-preserving path — anInt32block staysInt32(DenseUniTensorTest.normalize_int_typestill passes). - No new pragma and no deprecated call in the newly-written mutator; it reuses the existing pragma-wrapped
Norm()delegation.
Verified: the normalize* tests (incl. normalize_int_type), the full DenseUniTensor suite (308), and the full CPU suite all pass with 0 failures.
There was a problem hiding this comment.
There are heaps of GPU -> host -> GPU round trips elsewhere in Cytnx, some of them deliberately going through unified memory. IIRC Vectordot is one of the worst offenders. But staying on the device is not usually what you want here anyway; eigensolvers and krylov algebra are constructing small dense matrices that are pointless to put on the GPU.
The problem with this approach is
Tensor x(...., Types.Float);
x /= x.norm(); // auto-promotes to Double becase norm() returns double precisionA variant of this pattern hides Exp(Float) bugs in the Lanczos_Exp function by silently promoting to double, where Exp(Double) happens to work.
There was a problem hiding this comment.
Fixed in 8eb9401 — norm() now returns a Scalar instead of a fixed double, which addresses this directly.
Your example
Tensor x(..., Type.Float);
x /= x.norm(); // used to auto-promote to Doublenow stays Float. norm() returns a Scalar carrying the tensor's own precision (Float for Float/ComplexFloat, Double otherwise), so Tensor /= Scalar goes through Div's dtype-preserving path instead of the double-promoting one. Added a regression test asserting vf.norm().dtype() == Float and that vf /= vf.norm() keeps Float.
Python still gets a native float (the pybind binding converts the Scalar), per the direction that the Python surface shouldn't expose cytnx.Scalar — Python derives the type from dtype.
Internal callers that genuinely want a host double (the Block/Lanczos normalize paths) keep it via an explicit double(...) cast, so their numerics are unchanged.
And you're right about the latent Exp(Float) bug: linalg::Exp/Expf select the kernel by out.dtype() but feed it Tin's storage, so when the Float→Double astype fires, the Double kernel reads a Float buffer (wrong values + OOB read). It's masked precisely because callers promote to Double before reaching Exp. That's pre-existing and orthogonal to this PR, so I'll track it as a separate issue rather than fold it in here.
…undtrip Review of #1000: normalize_() computed linalg::norm(_block) (a host double) and wrote it back into a device Tensor via nrm.at({0}) = ..., forcing a device->host->device roundtrip (and a sync) for the scalar on GPU. Divide by this->Norm() instead: the non-deprecated internal virtual returns the norm as an on-device rank-1 {1} Tensor of the same dtype the manual nrm mirrored (Float for Float/ComplexFloat, Double otherwise), so the operation stays entirely on-device. Dividing by a Tensor still routes through linalg::iDiv's in-place, dtype-preserving path, so an Int32 block stays Int32 (DenseUniTensorTest. normalize_int_type). This also drops the deprecated linalg::Norm call the bot's suggestion would have reintroduced into the new mutator -- Norm()'s existing pragma-wrapped delegation already handles that for one release. normalize suite (incl. normalize_int_type) and full DenseUniTensor suite pass; full CPU suite 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code ReviewThree independent concerns: 🔴 Blocker — does not compile on Linux/GCCThree
The whole Verified correct
Findings1. 🟠 Silent Float->Double promotion in live Lanczos routines — the same dtype trap the PR fixed in
A Float-precision Lanczos solve now returns Double eigenvectors — a silent output-dtype change vs. the base. Complex/Double are unaffected; blast radius is small (Float solves are rare) but it's a genuine regression and inconsistent with the care taken in 2. 🟡 3. Note (pre-existing, out of scope): Block/BlockFermionic Accepted (intentional) — please documentThe VerdictThe conventions work is genuinely useful and the low-risk pieces (norm value semantics, deprecation shims, underscore renames, permute/reshape unification) are correct and well-tested. Two things to fix before merge: the Linux/GCC build break (blocker) and the silent Float->Double Lanczos promotion (which contradicts the PR's own Posted by Claude Code on behalf of @pcchen |
0861f01 to
340c8e8
Compare
|
norm() should return a Scalar so that type can be derived from Tensor.dtype() |
Review of #1000 (ianmccul): returning a fixed double from norm() made `x /= x.norm()` silently promote a Float tensor to Double through Div's Tensor/scalar path. Return a Scalar carrying the input's own precision instead (Float for Float/ComplexFloat, Double otherwise), so in-place division stays dtype-preserving. Tensor::norm(), UniTensor::norm() and both linalg::norm() overloads change double -> Scalar; the deprecated Norm() forms are unchanged. Python must not expose cytnx.Scalar, so the pybind norm bindings convert to a native float (double(self.norm())) -- Python still gets a plain float and derives type from dtype. Internal callers (Block/BlockFermionic normalize_/Norm, Lanczos_ER/Gnd/ Gnd_Ut/Exp) keep their prior double behavior via an explicit double(...) cast, so no algorithm's numerics change. Also fix a pre-existing GCC build failure of the Python module: the deprecated Tensor::Norm / UniTensor::Norm / UniTensor::combineBonds bindings had a #pragma GCC diagnostic inside the .def() chain, which GCC rejects ("'#pragma' is not allowed here"; only clang tolerated it). The suppression now lives in file-scope helper functions, leaving the chain pragma-free. Tests: C++ norm/Norm/normalize/Lanczos 69/69 (incl. Float/ComplexFloat); pytests 116 passed / 1 skipped (norm returns float; deprecated bindings still warn); dtype-preservation assertions added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d bindings The deprecated Norm/combineBonds/set_name/set_label/relabels/tag/convert_from Python bindings called PyErr_WarnEx without checking its return, then continued and returned a result. Under -W error::DeprecationWarning that turns the warning into a pending exception, so returning normally raised "SystemError: returned a result with an exception set" instead of the DeprecationWarning. Wrap every PyErr_WarnEx call as if (PyErr_WarnEx(...) < 0) throw py::error_already_set(); matching the already-correct site in symmetry_py.cpp, so the warning propagates cleanly under -W error and behaves unchanged otherwise. Verified: the deprecated Tensor/UniTensor/linalg bindings now raise DeprecationWarning (not SystemError) under -W error; norm_test.py and underscore_convention_test.py pass (22). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Conflict resolutions:
- Add/Sub/Mul/Div (UniTensor (+) scalar): took master's Type.type_promote()
path (the branch still had the old `dtype < dtype` min-enum rule); kept the
branch's set_name_.
- Block/BlockFermionic/DenseUniTensor relabel: kept the branch's set_label_
rename + master's return-style simplification.
- UniTensor_base: kept master's rank() metadata-consistency check + the branch's
set_name_/deprecated set_name.
- Norm.cpp: adopted master's rank-0 ({}) output and is_void/is_empty guards
(ZeroExtentLinalgTest requires Norm(zero-extent).at<float>({})); kept the
branch's norm()->Scalar. The UniTensor overloads now delegate to the
per-subclass UniTensor::Norm()/norm() (drops the branch's manual block loop,
which indexed .at({0}) and broke on rank-0).
- #934 elementwise policy: adopted master's AMENDED ruling -- keep +/-/+=/-=
guarded on matching metadata, remove */ *= /= (supersedes the branch's
remove-all). Harmonized master's adopted +/- lambdas to set_name_.
- Fixed branch-side ones()/zeros() scalar-arg calls (master removed the scalar
overload): DenseUniTensor_test dtype assertion and the Norm block accumulator.
Verified: full CPU test_main 1227 passed / 11 skipped / 0 failed; pytest
elementwise (master policy) + norm + underscore 42 passed; clang-format 14 clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1000 +/- ##
==========================================
+ Coverage 60.14% 60.38% +0.23%
==========================================
Files 229 229
Lines 31728 31866 +138
Branches 71 71
==========================================
+ Hits 19083 19241 +158
+ Misses 12623 12604 -19
+ Partials 22 21 -1
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 2 files with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
Brings in the latest master (98de76f): #1001 (Bond made immutable + BlockFermionicUniTensor signflip encapsulated via reset_signflip_/ erase_signflip_), #1047 (Exp/Expf consolidated into one dtype-preserving Exp; Expf/Expf_ deprecated), #1049 (UniTensor//UniTensor floor-division guarded), and the build-test-workflow / cross-revision-benchmark skills. Conflict resolution: - tests/DenseUniTensor_test.cpp: both sides added a test after combineBond_is_in_place -- this PR's combineBond_is_out_of_place and master's #846 aliasing regression (CombineBondForceDoesNotRewriteUserHeldBond). Kept both. Adaptations required by the merge (master's incoming tests were written against master's in-place combineBond(); this PR redefined bare combineBond() as out-of-place per #421/#422): - tests/DenseUniTensor_test.cpp + tests/BlockUniTensor_test.cpp: the #846 CombineBondForceDoesNotRewriteUserHeldBond tests assert the receiver itself mutates (ut.bonds()[0].dim() == combined), so they now call the in-place combineBond_() spelling instead of bare combineBond() (which now returns a new tensor and leaves the receiver unchanged). The #846 aliasing invariant is unchanged -- the in-place force path still clones the bond entry before force_combineBond_. - pytests/apply_test.py: master's newly-moved file called the deprecated .Norm().item(); migrated to .norm() (a native float in Python) to keep the suite clean under -W error::DeprecationWarning, matching this PR's migration of every other in-tree Norm() caller. Auto-merged cleanly (verified as a proper union, no dropped hunks): the PR's norm()->Scalar + underscore renames (set_name_/set_label_/tag_) coexist with master's Bond-immutable retype()/signflip refactor across the linalg decomposition files (Svd/Gesvd/Qr/Qdr/Rsvd/*_truncate); the floordiv guard (UniTensor//UniTensor raises, scalar // kept) sits with the amended #934 elementwise policy in unitensor_py.cpp; Exp consolidation + norm/Norm bindings coexist in linalg.hpp/linalg_py.cpp. Verification (CI-matching openblas-cpu, non-ASan): - C++ test_main: 1248 tests ran, 1237 passed / 11 skipped / 0 failed. - pybind module compiles clean; full pytests/ suite 185 passed / 1 skipped. - merge-adapted test files pass under -W error::DeprecationWarning. - clang-format v14 clean. Note (pre-existing, out of scope; flagged for a follow-up): under an ASan build, BlockUniTensor::combineBonds (src/BlockUniTensor.cpp:1958,1990) trips memcpy-param-overlap -- it memcpy()s an overlapping intra-vector shift where memmove() is required. This is latent UB unchanged by this merge (identical on master); the forward copy yields correct results, so it is benign on the non-ASan CI build and only ASan flags it. master's #846 test (combining two bonds of a 4-bond tensor, leaving a trailing bond) is the first case to surface it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
master advanced past the previous merge when PR #1011 (refactor/scalar-variant: Scalar PIMPL -> std::variant) landed. Re-merge to keep #1000 mergeable. Conflict resolution: - src/linalg/Norm.cpp: #1000 computed the rank-0 norm output dtype with an explicit if/else (ComplexDouble->Double, ComplexFloat->Float, else input-or- Double); #1011 introduced Type_class::norm_result_dtype() encapsulating the identical policy (is_float ? to_real : Double) and made it the single home of that rule, shared with the UniTensor normalize_ accumulators. Adopted the helper (Void is already guarded upstream at norm_impl's is_void check). Verified byte-for-byte equivalent dtype for every input type. Follow-through (keep the suite clean under -W error::DeprecationWarning): migrated the two remaining .Norm().item() callers the PR's own migration had missed -- pytests/unitensor_test.py and pytests/binding_inplace_test.py -- to .norm() (a native float in Python; same value). Surfaced because the rebuilt module shifted the DeprecationWarning-registry dedup order; harmless on CI (plain pytest) but now deterministically clean under -W error. Verification (CI-matching openblas-cpu, non-ASan): - C++ test_main: 1768 tests, 1757 passed / 11 skipped / 0 failed. - pybind rebuilds clean; full pytests/ suite passes under plain pytest; the merge-relevant files pass under -W error::DeprecationWarning. - clang-format v14 clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decisions
Maintainer rulings (yingjerkao, 2026-07-06) resolving #676, #335/#336/#381 (+ the #421/#422 merges), and #293. Three decision commits (below), reviewable independently, plus review follow-ups and a merge with
master(see Stacking / merge state):1.
norm()returnsScalar(C++) /float(Python) (#676)New snake_case
Tensor::norm(),UniTensor::norm(),linalg::norm()return aScalarcarrying the input's own precision (Float for Float/ComplexFloat, Double otherwise) — not a fixeddouble. This keepsx /= x.norm()dtype-preserving (routes throughDiv's Tensor/Scalar path) instead of silently promoting a Float tensor to Double (per @ianmccul's review). The Python bindings convert to a nativefloat— the Python surface must not exposecytnx.Scalar; Python derives type fromdtype(tracked for full removal in #1045). The oldNorm()/linalg::Normforms are[[deprecated]](C++) and warn viaDeprecationWarning(Python) for one release, unchanged in behavior. In-tree callers that genuinely want a host double keep it via an explicitdouble(...)cast, so their numerics are unchanged;normalize_()divides by the on-deviceNorm()tensor to stay both dtype-preserving and on-device.2. Underscore convention:
_= in-place returning self, everywhere (#335/#336/#381)Audit found 5 methods violating the rule. New canonical spellings:
set_name_,set_label_,tag_,convert_from_,combineBond_; old spellings remain as deprecation shims — exceptcombineBond(), whose bare name flips from in-place to out-of-place at the same signature (BREAKING, both languages), completing the #421/#422 API merge. Migration: usecombineBond_()for the old behavior.combineBonds(already deprecated in C++) now also warns in Python.3. Both call syntaxes on both classes (#293)
permute/reshapeaccept variadic and list forms on Tensor and UniTensor alike, including variadic string labels on UniTensor (ut.permute("a","b","c")). Additive; existing forms unchanged. Shared arg-parsing helpers live inpybind/pyint_dispatch.hppwith the overload-ordering rationale.Testing
46 new pytests (117 total on the chain, clean under
-W error::DeprecationWarning) + 11 gtests; C++ suite 1073 passed / 11 skipped / only the 4 pre-existing failures addressed by #977. A dtype regression innormalize_was caught by executed gtests during review and fixed.Review follow-ups (in this PR):
norm()changeddouble→Scalar/floatto fix the dtype-promotion footgun (above); moved the deprecatedNorm/combineBondspybind bindings'#pragma GCC diagnosticout of the.def()chains into file-scope helpers so the Python module builds under GCC (not only clang); made every deprecated binding'sPyErr_WarnExcheck its return (if (... < 0) throw py::error_already_set();) so they raise cleanly under-W error::DeprecationWarninginstead ofSystemError.Follow-ups (separate): remove the
cytnx.ScalarPython binding (#1045); fix the latentExp(Float)/Expfkernel bug (#1046, surfaced in @ianmccul's review — already fixed onmasterby 5d2a553); friendlierTypeErrorfor mixed/zero-arg permute calls.Stacking / merge state
Now targets
masterdirectly (the #986 → #991 → #997 stacked chain has landed). Merged latestmaster, resolving conflicts across the linalg scalar ops (adopted master'sType.type_promotepromotion over the branch's old min-enum rule), the underscore renames (set_name_/set_label_), andNorm(master's rank-0 scalar output +is_void/is_emptyguards, combined with this PR'snorm()→Scalar).UniTensor elementwise policy now follows master's amended #934 ruling:
+ - += -=are kept but guarded (require matching metadata — same type/rank/labels/rowrank/is_diag/bonds — otherwiseTypeError), and only* / *= /=are removed. This supersedes this branch's earlier "remove all elementwise" version of #934.Post-merge verification: full CPU
test_main1227 passed / 11 skipped / 0 failed; pytest (elementwise guarded-policy + norm + underscore) 42 passed; clang-format v14 clean.🤖 Generated with Claude Code