Skip to content

feat(api)!: norm()->Scalar, underscore convention for mutators, unified permute/reshape syntax#1000

Merged
yingjerkao merged 10 commits into
masterfrom
feat/api-conventions
Jul 14, 2026
Merged

feat(api)!: norm()->Scalar, underscore convention for mutators, unified permute/reshape syntax#1000
yingjerkao merged 10 commits into
masterfrom
feat/api-conventions

Conversation

@yingjerkao

@yingjerkao yingjerkao commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

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() returns Scalar (C++) / float (Python) (#676)

New snake_case Tensor::norm(), UniTensor::norm(), linalg::norm() return a Scalar carrying the input's own precision (Float for Float/ComplexFloat, Double otherwise) — not a fixed double. This keeps x /= x.norm() dtype-preserving (routes through Div's Tensor/Scalar path) instead of silently promoting a Float tensor to Double (per @ianmccul's review). The Python bindings convert to a native float — the Python surface must not expose cytnx.Scalar; Python derives type from dtype (tracked for full removal in #1045). The old Norm()/linalg::Norm forms are [[deprecated]] (C++) and warn via DeprecationWarning (Python) for one release, unchanged in behavior. In-tree callers that genuinely want a host double keep it via an explicit double(...) cast, so their numerics are unchanged; normalize_() divides by the on-device Norm() 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 — except combineBond(), 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: use combineBond_() for the old behavior. combineBonds (already deprecated in C++) now also warns in Python.

3. Both call syntaxes on both classes (#293)

permute/reshape accept 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 in pybind/pyint_dispatch.hpp with 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 in normalize_ was caught by executed gtests during review and fixed.

Review follow-ups (in this PR): norm() changed doubleScalar/float to fix the dtype-promotion footgun (above); moved the deprecated Norm/combineBonds pybind bindings' #pragma GCC diagnostic out of the .def() chains into file-scope helpers so the Python module builds under GCC (not only clang); made every deprecated binding's PyErr_WarnEx check its return (if (... < 0) throw py::error_already_set();) so they raise cleanly under -W error::DeprecationWarning instead of SystemError.

Follow-ups (separate): remove the cytnx.Scalar Python binding (#1045); fix the latent Exp(Float)/Expf kernel bug (#1046, surfaced in @ianmccul's review — already fixed on master by 5d2a553); friendlier TypeError for mixed/zero-arg permute calls.

Stacking / merge state

Now targets master directly (the #986#991#997 stacked chain has landed). Merged latest master, resolving conflicts across the linalg scalar ops (adopted master's Type.type_promote promotion over the branch's old min-enum rule), the underscore renames (set_name_/set_label_), and Norm (master's rank-0 scalar output + is_void/is_empty guards, combined with this PR's norm()→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 — otherwise TypeError), and only * / *= /= are removed. This supersedes this branch's earlier "remove all elementwise" version of #934.

Post-merge verification: full CPU test_main 1227 passed / 11 skipped / 0 failed; pytest (elementwise guarded-policy + norm + underscore) 42 passed; clang-format v14 clean.

🤖 Generated with Claude Code

yingjerkao and others added 4 commits July 6, 2026 20:07
…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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/DenseUniTensor.cpp
Comment on lines +1275 to +1289
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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
  }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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->device scalar roundtrip/sync. ✅ (your point)
  • The divisor is a rank-1 {1} Tensor with the exact dtype the old manual nrm mirrored (Float for Float/ComplexFloat, Double otherwise), so Tensor /= still routes through linalg::iDivs in-place, dtype-preserving path — an Int32 block stays Int32 (DenseUniTensorTest.normalize_int_type still 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 precision

A variant of this pattern hides Exp(Float) bugs in the Lanczos_Exp function by silently promoting to double, where Exp(Double) happens to work.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8eb9401norm() 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 Double

now 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>
@pcchen

pcchen commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Three independent concerns: norm()->double (#676), the _=in-place underscore convention (#335/#336/#381), and unified variadic/list permute/reshape (#293).

🔴 Blocker — does not compile on Linux/GCC

Three #pragma GCC diagnostic push/ignored/pop blocks are placed inside the chained .def(...).def(...) expression (to silence -Wdeprecated-declarations on the deprecated Norm/combineBonds bindings):

  • pybind/tensor_py.cpp:1725-1740
  • pybind/unitensor_py.cpp:1014-1044 and 1991-2002

The whole py::class_<...>(...).def(...)....def(...); is a single expression; GCC rejects a #pragma mid-expression (error: '#pragma' is not allowed here, then expected primary-expression before '.'). macOS Clang tolerates it, so the local build + both macOS wheels passed while both Ubuntu wheels failed. Fix by moving the deprecated bindings out of the chain (split into a separate cls.def(...) statement so the pragma sits between statements) or by binding a file-scope helper wrapped in the pragma.

Verified correct

  • norm()->double value + shimslinalg::norm applies .item().real() exactly once; C++ Norm()/linalg::Norm shims still return the rank-0 Tensor unchanged; Python Norm warns then returns the same value/type. normalize_()'s dtype-preservation is genuinely fixed on DenseUniTensor (divides by a rank-0 Tensor via the dtype-preserving iDiv, not the promoting double/Scalar path — guarded by a new gtest).
  • combineBond flip — implementation is correct (out-of-place clones first; combineBond_ is the renamed in-place; both bound in Python; no internal caller relied on old behavior).
  • Underscore shims (set_name_/set_label_/tag_/convert_from_) are warn-only deprecation forwards; permute/reshape variadic helpers (parse_index_args/is_string_args) are correct and additive.

Findings

1. 🟠 Silent Float->Double promotion in live Lanczos routines — the same dtype trap the PR fixed in normalize_, but missed at these callers. Replacing X.Norm() (dtype-preserving) with X.norm() (a double) means dividing a Float tensor by it promotes to Double (type_promote(Double,Float)=Double):

  • src/linalg/Lanczos_Gnd.cpp:25,119psi_1 = Tin.clone() / Tin.norm();
  • src/linalg/Lanczos_ER.cpp:36,47,52,85,94
  • examples example/DMRG/*.py, example/iTEBD/iTEBD_tag.py

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 normalize_. Fix: keep these divisions dtype-preserving (divide by the rank-0 Tensor / Scalar form, not the double).

2. 🟡 src/Tensor.cpp:883 Tensor::Norm() calls the now-[[deprecated]] linalg::Norm without the pragma guard the PR applied everywhere else -> emits a deprecation warning on every build. Harmless (no -Werror) but inconsistent; route it through the internal non-deprecated path or add the guard.

3. Note (pre-existing, out of scope): Block/BlockFermionic normalize_ divide by a Scalar (promoting path), so the int-dtype guarantee Dense now has doesn't hold for Block — flagging for awareness only.

Accepted (intentional) — please document

The combineBond() semantic flip (same name+signature, in-place -> out-of-place) is a deliberate, accepted breaking change to enforce the project's _=in-place convention (bare combineBond is now out-of-place on both Bond and UniTensor; use combineBond_() for the old in-place behavior). It cannot carry a deprecation warning (identical name/signature), and only ref-binding misuse is caught by the return-type change (UniTensor&->UniTensor) — the common ut.combineBond(...)-for-side-effect case silently stops mutating. Action: make this prominent in the migration guide / release notes with the combineBond -> combineBond_ mapping, since it is a silent break for external code. (Consider fully removing combineBonds, not just warning, to reduce the combineBond/combineBonds confusability.)

Verdict

The 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 normalize_ fix). The combineBond flip is accepted as intentional — just needs prominent documentation. Request changes on the build + the Lanczos dtype promotion.

Posted by Claude Code on behalf of @pcchen

@yingjerkao

Copy link
Copy Markdown
Collaborator Author

norm() should return a Scalar so that type can be derived from Tensor.dtype()

Base automatically changed from refactor/unitensor-drop-elementwise to master July 10, 2026 05:30
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>
@yingjerkao yingjerkao changed the title feat(api)!: norm()->double, underscore convention for mutators, unified permute/reshape syntax feat(api)!: norm()->Scalar, underscore convention for mutators, unified permute/reshape syntax Jul 13, 2026
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

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.52610% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.38%. Comparing base (9d9bf05) to head (343452f).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
pybind/unitensor_py.cpp 79.79% 20 Missing ⚠️
src/BlockFermionicUniTensor.cpp 50.00% 4 Missing ⚠️
src/linalg/Lanczos_ER.cpp 33.33% 4 Missing ⚠️
pybind/pyint_dispatch.hpp 81.25% 3 Missing ⚠️
src/linalg/Qdr.cpp 0.00% 3 Missing ⚠️
include/UniTensor.hpp 95.45% 2 Missing ⚠️
pybind/linalg_py.cpp 88.23% 2 Missing ⚠️
src/BlockUniTensor.cpp 75.00% 2 Missing ⚠️
src/linalg/Qr.cpp 0.00% 2 Missing ⚠️
pybind/tensor_py.cpp 92.85% 1 Missing ⚠️
... and 3 more
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     
Flag Coverage Δ
cpp 60.33% <81.52%> (+0.23%) ⬆️
python 64.13% <ø> (+0.52%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
C++ backend 58.03% <80.58%> (+0.06%) ⬆️
Python bindings 74.26% <82.19%> (+0.89%) ⬆️
Python package 64.13% <ø> (+0.52%) ⬆️
Files with missing lines Coverage Δ
include/Tensor.hpp 88.68% <ø> (ø)
src/DenseUniTensor.cpp 86.81% <100.00%> (+0.01%) ⬆️
src/Tensor.cpp 31.89% <100.00%> (-0.58%) ⬇️
src/UniTensor_base.cpp 48.90% <100.00%> (-0.63%) ⬇️
src/linalg/Add.cpp 51.17% <100.00%> (ø)
src/linalg/Div.cpp 20.39% <100.00%> (-3.58%) ⬇️
src/linalg/Lanczos_Exp.cpp 97.50% <ø> (ø)
src/linalg/Lanczos_Gnd.cpp 86.41% <100.00%> (ø)
src/linalg/Lanczos_Gnd_Ut.cpp 89.65% <100.00%> (ø)
src/linalg/Mul.cpp 35.23% <100.00%> (ø)
... and 17 more

... and 2 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9d9bf05...343452f. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

yingjerkao and others added 2 commits July 14, 2026 14:26
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>
@yingjerkao
yingjerkao merged commit 6600059 into master Jul 14, 2026
21 of 22 checks passed
@yingjerkao
yingjerkao deleted the feat/api-conventions branch July 14, 2026 09:23
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.

3 participants