Skip to content

fix(pybind)!: guard UniTensor//UniTensor floor-division (closes #934 hole from #997) - #1049

Merged
yingjerkao merged 1 commit into
masterfrom
fix/unitensor-floordiv-bypass
Jul 13, 2026
Merged

fix(pybind)!: guard UniTensor//UniTensor floor-division (closes #934 hole from #997)#1049
yingjerkao merged 1 commit into
masterfrom
fix/unitensor-floordiv-bypass

Conversation

@yingjerkao

Copy link
Copy Markdown
Collaborator

Problem

#997 removed elementwise UniTensor ⊘ UniTensor division from the Python surface — but only for the true-division dunders. Python's / maps to __truediv__, which #997 correctly guarded (raises TypeError). Python's floor-division // maps to a distinct dunder, __floordiv__, and the UniTensor⊗UniTensor overloads there were left routing straight to linalg::Div / Div_:

ut1 / ut2    # TypeError  (removed, as #997 intended)
ut1 // ut2   # silently ran linalg::Div(ut1, ut2)   ← back-door
ut1 //= ut2  # silently ran ut1.Div_(ut2)           ← back-door

So // and //= were a silent back-door for the exact basis-dependent Hadamard quotient that / was made to reject (#934 — no well-defined tensor-network meaning, typically produces inf/nan). The floor-division path had no test coverage in binding_unitensor_elementwise_test.py, which is why the gap slipped through #997.

(There is no floor-multiply operator, so the * removal has no analogous hole — this is division-only.)

Fix

The two UniTensor⊗UniTensor floordiv overloads now raise_unitensor_elementwise_removed, mirroring their __truediv__ / __itruediv__ siblings exactly and reusing the same kUniTensorDivRemovedGuidance message:

  • pybind/unitensor_py.cpp __floordiv__(UniTensor, UniTensor) → raises TypeError (//)
  • pybind/unitensor_py.cpp __ifloordiv__(UniTensor, UniTensor) → raises TypeError (//=)

Scalar floordiv is retained (ut // 2.0, ut //= 2.0), matching how scalar / is kept — only the two-UniTensor overloads change.

Testing

pytests/binding_unitensor_elementwise_test.py — added:

  • test_floordiv_unitensor_unitensor_raisesut1 // ut2 raises TypeError
  • test_ifloordiv_unitensor_unitensor_raisesut1 //= ut2 raises TypeError
  • test_scalar_floordiv_unitensor_still_works — pins the scope boundary (scalar // still works)

Rebuilt openblas-cpu and ran the suite: 23 passed (was 20).

Breaking change

ut1 // ut2 and ut1 //= ut2 now raise TypeError instead of performing elementwise division — this completes the removal announced as a BREAKING CHANGE in #997.

Closes the floor-division gap in #934 / #997.

🤖 Generated with Claude Code

…hole from #997)

#997 removed elementwise UniTensor/UniTensor division by guarding the
`__truediv__` / `__itruediv__` dunders (`/`, `/=`), but Python's
floor-division maps to the *distinct* `__floordiv__` / `__ifloordiv__`
dunders, which were left routing straight to `linalg::Div` / `Div_`.

That left `ut1 // ut2` and `ut1 //= ut2` as a silent back-door for the
exact Hadamard quotient `ut1 / ut2` was made to reject -- a basis-dependent
op with no tensor-network meaning (#934). The floordiv path had no test
coverage in binding_unitensor_elementwise_test.py, so the gap went unnoticed.

Fix: the two `UniTensor⊗UniTensor` floordiv overloads now
`raise_unitensor_elementwise_removed`, mirroring their truediv siblings and
reusing the same `kUniTensorDivRemovedGuidance` message. Scalar floordiv
(`ut // 2.0`, `ut //= 2.0`) is retained, matching how scalar `/` is kept.

Tests: adds test_floordiv_unitensor_unitensor_raises,
test_ifloordiv_unitensor_unitensor_raises, and
test_scalar_floordiv_unitensor_still_works (scope boundary). Full suite:
23 passed.

BREAKING CHANGE: `ut1 // ut2` and `ut1 //= ut2` now raise TypeError instead
of performing elementwise division -- completing the removal announced in
#997.

Co-Authored-By: Claude Opus 4.8 (1M context) <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 removes the elementwise floor-division (// and //=) operators between two UniTensor objects, raising a TypeError with guidance instead. This prevents bypassing the restriction on elementwise division (/ and /=) which was previously removed. Scalar floor-division (e.g., ut // 2.0) remains supported and routes to linalg::Div. Corresponding unit tests have been added to verify that the two-UniTensor floor-division raises the expected error and that scalar floor-division continues to work. 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.

@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 57.72%. Comparing base (33f5c23) to head (ea94e76).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1049      +/-   ##
==========================================
+ Coverage   57.71%   57.72%   +0.01%     
==========================================
  Files         229      229              
  Lines       33468    33471       +3     
  Branches       71       71              
==========================================
+ Hits        19315    19321       +6     
+ Misses      14132    14129       -3     
  Partials       21       21              
Flag Coverage Δ
cpp 57.65% <100.00%> (+0.01%) ⬆️
python 63.35% <ø> (ø)

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

Components Coverage Δ
C++ backend 55.47% <ø> (ø)
Python bindings 72.07% <100.00%> (+0.08%) ⬆️
Python package 63.35% <ø> (ø)
Files with missing lines Coverage Δ
pybind/unitensor_py.cpp 71.75% <100.00%> (+0.25%) ⬆️

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 33f5c23...ea94e76. 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
yingjerkao merged commit cf69904 into master Jul 13, 2026
19 checks passed
@yingjerkao
yingjerkao deleted the fix/unitensor-floordiv-bypass branch July 13, 2026 07:02
yingjerkao added a commit that referenced this pull request Jul 14, 2026
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>
yingjerkao added a commit that referenced this pull request Jul 15, 2026
…ue-division (#941)

Reimplements the deferred half of #1015 on current master, which had since
refactored the same six arithmetic files for rank-zero support (#1026),
zero-extent (#1043), and type_promote (#984).

- Convert CPU out-of-place AND in-place Add/Sub/Mul/Div/Cpr/Mod to typed
  std::visit over as_storage_variant() with storage_cast<TO> and typed
  *InternalImpl<TO,TL,TR> kernels; the dead legacy dispatch tables are removed.
- True division (#941): `/` and `/=` yield a floating result (int/int -> Double)
  via make_floating_point_dtype; the rank-zero / broadcast output still routes
  through #1026's init_broadcast_binary_output (seeded with the floating dtype).
- Mixed-dtype in-place TENSOR ops promote the LHS via storage_as_type_or_replace
  (no more truncation into the narrower LHS storage -- #941's core bug).
- Weak-scalar (#980, per #1015's binding ruling): `tensor op= python-scalar`
  (a rank-0 RHS via scalar_as_rank0_tensor) PRESERVES the LHS dtype for
  Add/Sub/Mul and follows true-division for Div (Int64 /= 2.0 -> Double,
  Float /= 3.0 -> Float), keyed on Rt.rank()==0 in DispatchInplaceArithmeticCPU;
  complex-into-real still throws.
- floordiv conforms to #1049 (scalar `ut // 2.0` kept, `ut1 // ut2` raises);
  #1015's original unbind-all is dropped.
- normalize_() now promotes an integer block to Double under true division; its
  stale "stays Int32" comment is updated. Mod keeps an eager is_complex guard so
  a zero-extent complex Mod still throws (ZeroExtentLinalgTest).

CPU only; GPU stays on the legacy dtype tables behind the unchanged public API
(#1013 tracks the GPU conversion).

Testing: full openblas-cpu (non-ASan) test_main 1784 passed / 11 skipped /
0 failed -- MixedDtypeArithmetic regressions, weak-scalar Tensor.ScalarInplace*,
ZeroExtentLinalgTest, and DenseUniTensorTest true-division/normalize all green;
clang-format v14 clean.

Reimplements ff30d00, deferred from #1015 (#941).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
yingjerkao added a commit that referenced this pull request Jul 15, 2026
…ue-division (#941)

Reimplements the deferred half of #1015 on current master, which had since
refactored the same six arithmetic files for rank-zero support (#1026),
zero-extent (#1043), and type_promote (#984).

- Convert CPU out-of-place AND in-place Add/Sub/Mul/Div/Cpr/Mod to typed
  std::visit over as_storage_variant() with storage_cast<TO> and typed
  *InternalImpl<TO,TL,TR> kernels; the dead legacy dispatch tables are removed.
- True division (#941): `/` and `/=` yield a floating result (int/int -> Double)
  via make_floating_point_dtype; the rank-zero / broadcast output still routes
  through #1026's init_broadcast_binary_output (seeded with the floating dtype).
- Mixed-dtype in-place TENSOR ops promote the LHS via storage_as_type_or_replace
  (no more truncation into the narrower LHS storage -- #941's core bug).
- Weak-scalar (#980, per #1015's binding ruling): `tensor op= python-scalar`
  (a rank-0 RHS via scalar_as_rank0_tensor) PRESERVES the LHS dtype for
  Add/Sub/Mul and follows true-division for Div (Int64 /= 2.0 -> Double,
  Float /= 3.0 -> Float), keyed on Rt.rank()==0 in DispatchInplaceArithmeticCPU;
  complex-into-real still throws.
- floordiv conforms to #1049 (scalar `ut // 2.0` kept, `ut1 // ut2` raises);
  #1015's original unbind-all is dropped.
- normalize_() now promotes an integer block to Double under true division; its
  stale "stays Int32" comment is updated. Mod keeps an eager is_complex guard so
  a zero-extent complex Mod still throws (ZeroExtentLinalgTest).

CPU only; GPU stays on the legacy dtype tables behind the unchanged public API
(#1013 tracks the GPU conversion).

Testing: full openblas-cpu (non-ASan) test_main 1784 passed / 11 skipped /
0 failed -- MixedDtypeArithmetic regressions, weak-scalar Tensor.ScalarInplace*,
ZeroExtentLinalgTest, and DenseUniTensorTest true-division/normalize all green;
clang-format v14 clean.

Reimplements ff30d00, deferred from #1015 (#941).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
yingjerkao added a commit that referenced this pull request Jul 15, 2026
)

Address PR #1056 review (Codex): this PR unbinds Tensor `__floordiv__` /
`__rfloordiv__` / `__ifloordiv__` (and the keep-set-generated
`c__ifloordiv__`) in pybind/tensor_py.cpp, but the shipped PEP 561 stub
cytnx/cytnx/__init__.pyi still advertised those overloads. mypy/Pylance
would therefore accept `t // 2` even though it now raises TypeError at
runtime.

Remove exactly the Tensor floordiv-family entries from the stub, each
verified absent from the built module (`__floordiv__`, `__rfloordiv__`,
`__ifloordiv__`, `c__ifloordiv__`). This is a scoped surgical edit rather
than a full `tools/generate_stubs.py` regeneration: this environment only
has Python 3.12 (the committed stubs are generated with the lowest supported
3.10) plus a different numpy, so a wholesale regen emits ~2600 lines of
unrelated tooling/version churn. UniTensor scalar floordiv is intentionally
kept (#1049) and left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
yingjerkao added a commit that referenced this pull request Jul 21, 2026
Problem: cytnx.Scalar leaked C++'s Scalar value type onto the Python
surface. Python code should work with native numbers and derive dtype
from a tensor/storage's dtype(), never handle a Scalar object.

Fix:
- Drop the Scalar binding entirely (delete pybind/scalar_py.cpp, its
  scalar_binding() decl/call in cytnx.cpp, and its CMakeLists entry).
- Remove the redundant accept-Scalar arithmetic/compare overloads from
  Tensor (tensor_py.cpp) and UniTensor (unitensor_py.cpp). Each has a
  native double/complex128 sibling plus the numpy_scalar/py::int_ keep
  set, so plain Python numbers still dispatch unchanged -- including
  scalar UniTensor '//' and '%', kept per #1049.
- Migrate the linalg coefficients that were Scalar (Ger, Gemm, Gemm_,
  Lanczos_Exp) to native real overloads plus a complex_arg.hpp
  ComplexArg fallback, matching the existing ExpH/ExpM pattern so a real
  argument stays real and a Python complex is accepted on 3.10.
- Delete the now-obsolete Python Scalar tests (pytests/scalar_variant_test.py
  and the Scalar-constructor cases in keep_set_dispatch_test.py); the C++
  variant behaviour stays covered by tests/Scalar_test.cpp. Update the
  __ne__/reverse-op tests to use native operands.
- Regenerate the committed .pyi stubs (Scalar class removed; Ger/Gemm/
  Gemm_/Lanczos_Exp gain real/complex overloads).

Testing: pytest pytests/ -> 260 passed, 1 skipped. Smoke-checked that
cytnx.Scalar is gone, Tensor/UniTensor scalar arithmetic (incl. ut//2.0),
and Ger/Gemm/Gemm_/Lanczos_Exp accept native ints/floats/complex with the
expected result dtypes. clang-format v14 clean.

Closes #1045.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
yingjerkao added a commit that referenced this pull request Jul 22, 2026
)

Problem: cytnx.Scalar leaked C++'s Scalar value type onto the Python
surface. Python code should work with native numbers and derive dtype
from a tensor/storage's dtype(), never handle a Scalar object.

Fix:
- Drop the Scalar binding entirely (delete pybind/scalar_py.cpp, its
  scalar_binding() decl/call in cytnx.cpp, and its CMakeLists entry).
- Remove the redundant accept-Scalar arithmetic/compare overloads from
  Tensor (tensor_py.cpp) and UniTensor (unitensor_py.cpp). Each has a
  native double/complex128 sibling plus the numpy_scalar/py::int_ keep
  set, so plain Python numbers still dispatch unchanged -- including
  scalar UniTensor '//' and '%', kept per #1049.
- Migrate the linalg coefficients that were Scalar (Ger, Gemm, Gemm_,
  Lanczos_Exp) to native real overloads plus a complex_arg.hpp
  ComplexArg fallback, matching the existing ExpH/ExpM pattern so a real
  argument stays real and a Python complex is accepted on 3.10.
- Delete the now-obsolete Python Scalar tests (pytests/scalar_variant_test.py
  and the Scalar-constructor cases in keep_set_dispatch_test.py); the C++
  variant behaviour stays covered by tests/Scalar_test.cpp. Update the
  __ne__/reverse-op tests to use native operands.
- Regenerate the committed .pyi stubs (Scalar class removed; Ger/Gemm/
  Gemm_/Lanczos_Exp gain real/complex overloads).

Testing: pytest pytests/ -> 260 passed, 1 skipped. Smoke-checked that
cytnx.Scalar is gone, Tensor/UniTensor scalar arithmetic (incl. ut//2.0),
and Ger/Gemm/Gemm_/Lanczos_Exp accept native ints/floats/complex with the
expected result dtypes. clang-format v14 clean.

Closes #1045.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant