Bind ExpH/ExpM with per-dtype overloads + Python-layer fixes (toward clean stubtest)#915
Conversation
There was a problem hiding this comment.
Code Review
This pull request simplifies and consolidates the Python bindings for ExpH and ExpM by replacing numerous type-specific overloads with a single overload accepting cytnx::Scalar, supported by implicit conversions for NumPy scalars and a C++ dispatch helper. It also cleans up unused imports, refactors type stubs, and adds unit tests for the new scalar handling. The review feedback highlights two key improvement opportunities: first, the return_indices parameter in getDegeneracy is currently ignored and should be respected; second, py::numpy_scalar<bool> should be registered for implicit conversion to Scalar to prevent runtime type errors with NumPy boolean scalars.
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.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6f1f3cc9d3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4fb440de6f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
I didn't really know what mypy's stubtest tests and tlthe best solution either. This PR may not correct. |
94d54c9 to
bb32d2d
Compare
c7473d7 to
6700547
Compare
bb32d2d to
5bd01bb
Compare
|
Heads-up from running the full
This is unrelated to the changes here: TDVP drives time evolution through — Claude Generated by Claude Code |
6700547 to
bffc010
Compare
25a5894 to
55031b7
Compare
bffc010 to
c1e1f45
Compare
55031b7 to
f182481
Compare
@claude: When examining failed tests, do not start by questioning the tolerance value. You should first inspect carefully the tested code. In this case, the TDVP test is a trivially local imaginary-time evolution problem that should, in principle, converge to essentially numerical precision. In fact the @ Humans: Recommend putting above advice into local CLAUDE.md or other AI-agent instructions. I note that the PR branch has since been rebased onto current master. In this case it is essentially a rediscovery of #888. After #889 and #892 that test passes with tolerance around 4.21e-8. And that is primarily due to the relatively short imaginary time evolution. So actually the tolerance is weaker than it needs to be, and it isn't yet a sharp test of the numerics. |
|
If the goal is to bind It does not make much sense to split the Python binding by dtype, immediately fold those overloads back into a runtime-selected Cytnx dtype in C++, and then dispatch again at runtime to the backend implementation. That keeps the fragile runtime dtype path in the middle of the call chain. We have already seen with As a general design rule, if the Python binding needs one overload per dtype, then the C++ side should also have strongly typed overloads or template instantiations for those dtypes. The binding should dispatch into those typed C++ functions directly, rather than funneling back through a runtime dtype and an untyped backend dispatch path. |
725a8dd to
37e3210
Compare
6832f72 to
bab016c
Compare
37e3210 to
9e7e777
Compare
bab016c to
3088d86
Compare
linalg.ExpH and linalg.ExpM were bound once per C++ scalar dtype -- the native cytnx_* types plus py::numpy_scalar<...> variants, roughly twenty overloads per function across the UniTensor and Tensor signatures. pybind11-stubgen collapses several distinct C++ types onto the same Python annotation, so the generated stub carried duplicate @typing.overload signatures that mypy rejects with overload-cannot-match. Keep one binding per Python-distinguishable dtype and drop the redundant ones: - A Python float/complex already binds to the cytnx_double/cytnx_complex128 overloads, and np.float64/np.complex128 are builtin subclasses caught by those same overloads, so the cytnx_float, cytnx_complex64, numpy_scalar<double> and numpy_scalar<complex<double>> overloads were unreachable duplicates. - The fixed-width integer overloads all accept the same Python int, so replace them with a single py::int_ overload that dispatches on magnitude to the int64 or uint64 kernel. Python int is arbitrary precision, which no single fixed-width C++ overload can cover, so the runtime branch keeps the whole range behind one signature. Under pybind11 3.0.4 the kept overloads render as the numpy.* scalar types, `int` (py::int_), `typing.SupportsFloat | typing.SupportsIndex` (cytnx_double), and `typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex` (cytnx_complex128). The complex128 annotation is a supertype of the cytnx_double and integer ones, but it is registered last, so every earlier narrower overload stays reachable and mypy reports no overload-cannot-match. A bind_exp_scalar helper shares the registration across the four signatures. Two `b` defaults are supplied through py::arg_v with an explicit repr so the stub stays valid against the widened 3.0.4 annotations, rather than letting pybind11 format the default itself: - The numpy_scalar overloads use a `numpy.<type>(...)` repr. Otherwise pybind11 formats the default from the numpy scalar's own repr -- `np.<type>(...)` under numpy 2.x -- an unimported `np` alias that mypy rejects; naming the imported `numpy` module lets stubgen evaluate it and emit an elided `...`. - The cytnx_complex128 overload uses a literal `...` repr. pybind11 3.0.4 annotates the parameter `SupportsComplex | SupportsFloat | SupportsIndex`, which does not include the builtin `complex` (typeshed's `complex` has no `__complex__`), so a `0j` default would be rejected as an incompatible default. A `numpy.complex128(...)` repr would also elide, but it would misdescribe the default since this overload accepts a Python `complex`, not a numpy scalar, so `...` is used to state "elided" without claiming a type. The defaults still exist at runtime, so mypy.stubtest is unaffected, and tools/generate_stubs.py needs no special-casing for these arguments. numpy complex64 scalars are routed through the complex128 kernel because cytnx::linalg::Exp returns incorrect values for complex64 tensors (#914), on which both ExpH and ExpM depend; drop that reroute once that is fixed. Add a regression test that ExpH and ExpM accept Python and numpy scalars of every supported dtype, individually and in mixed (a, b) pairs, and match a NumPy eigendecomposition reference, with a dedicated complex64-matches-complex128 case. Co-Authored-By: Claude <noreply@anthropic.com>
3088d86 to
247f104
Compare
Context
Stacked on #912 (rebased on
masterafter #922 removed thetn_algobindings; stubs generated against the pinned pybind11 3.0.4). Works toward a cleanmypy.stubtestrun againstcytnx.cytnx. stubtest's dominant complaint is theoverload-cannot-matcherrors: many scalar-accepting functions are bound once per C++ scalar dtype, and pybind11-stubgen renders several distinct C++ types onto the same Python type, producing duplicate@typing.overloadsignatures. This PR is the first increment (ExpH/ExpM); it does not make stubtest fully green yet (see "Remaining").What this PR does
Bind
linalg.ExpH/ExpMwith one overload per Python-distinguishable dtype. Each function was bound ~20× per C++ scalar dtype, and stubgen collapses several of those onto the same Python type, so the stub carried duplicate overloads. Keep one binding per Python-distinguishable dtype and drop the dead/redundant ones:float/complexalready binds to thecytnx_double/cytnx_complex128overloads, andnp.float64/np.complex128are builtin subclasses caught by those same overloads, socytnx_float,cytnx_complex64,numpy_scalar<double>andnumpy_scalar<complex<double>>were unreachable duplicates.int, so replace them with a singlepy::int_overload that dispatches on magnitude to theint64/uint64kernel — one signature covering the full arbitrary-precision Python-int range that no single fixed-width C++ overload can.Under pybind11 3.0.4 the kept overloads render as the
numpy.*scalar types,int(py::int_),typing.SupportsFloat | typing.SupportsIndex(cytnx_double), andtyping.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex(cytnx_complex128). The complex128 annotation is a supertype of the narrower ones, but it is registered last, so each earlier overload stays reachable andlinalg.pyiis mypy-clean forExpH/ExpM(nooverload-cannot-match).Keep the scalar defaults valid at the binding site, not via a stub rewrite. Each
bdefault is passed throughpy::arg_vwith an explicitnumpy.<type>(...)repr that stubgen evaluates and emits as an elided..., valid against any annotation, for two reasons:np.<type>(...)under numpy 2.x), an unimportednpalias that mypy rejects;cytnx_complex128overload — pybind11 3.0.4'sSupportsComplex | SupportsFloat | SupportsIndexannotation excludes the builtincomplex(typeshed'scomplexhas no__complex__), so a literal0jdefault would be flagged as an incompatible default.The defaults still exist at runtime, so stubtest is unaffected and
tools/generate_stubs.pyneeds no special-casing.Numerical note / issue #914
linalg.Expreturns incorrect values for complex64 (ComplexFloat) tensors, which bothExpHandExpMdepend on. To preserve behaviour, numpy complex64 scalars are routed through the complex128 kernel; this is documented in code and tracked in #914, and the reroute can be removed once the kernel is fixed.Remaining (follow-up PR)
After this PR
linalg.pyiis mypy-clean, but stubtest still cannot run a clean comparison: ~5599overload-cannot-matcherrors remain, now all in__init__.pyi— the Tensor/UniTensor operators (__add__,__mul__,__eq__, in-place/reverse variants,__setitem__, …), each bound ~24× per dtype, plus the generated__hash__/__eq__markers. They need the same binding-site treatment this PR applies toExpH/ExpM; cleaning them up is the bulk of the work and a dedicated follow-up, after which CI can run stubtest.Testing
pytest pytests/linalg_exp_test.py: 26 passed —ExpH/ExpMaccept every supported Python and numpy scalar dtype individually and in mixed(a, b)pairs, matched against a NumPy eigendecomposition reference, including the complex64→complex128 case.linalg.pyiis mypy-clean:python -m mypy.stubtest cytnx.cytnxreports zero errors for it, and regenerating the stubs is idempotent (thearg_vdefaults reproduce the committed.pyibyte-for-byte).ExpH/ExpMkernels are untouched.🤖 Generated with Claude Code