Skip to content

Bind ExpH/ExpM with per-dtype overloads + Python-layer fixes (toward clean stubtest)#915

Draft
IvanaGyro wants to merge 1 commit into
claude/pybind11-stubgen-stubs-ebny1cfrom
claude/fix-stubtest-errors
Draft

Bind ExpH/ExpM with per-dtype overloads + Python-layer fixes (toward clean stubtest)#915
IvanaGyro wants to merge 1 commit into
claude/pybind11-stubgen-stubs-ebny1cfrom
claude/fix-stubtest-errors

Conversation

@IvanaGyro

@IvanaGyro IvanaGyro commented Jun 18, 2026

Copy link
Copy Markdown
Member

Context

Stacked on #912 (rebased on master after #922 removed the tn_algo bindings; stubs generated against the pinned pybind11 3.0.4). Works toward a clean mypy.stubtest run against cytnx.cytnx. stubtest's dominant complaint is the overload-cannot-match errors: 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.overload signatures. 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/ExpM with 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:

    • 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 cytnx_float, cytnx_complex64, numpy_scalar<double> and numpy_scalar<complex<double>> 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/uint64 kernel — 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), and typing.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 and linalg.pyi is mypy-clean for ExpH/ExpM (no overload-cannot-match).

  • Keep the scalar defaults valid at the binding site, not via a stub rewrite. Each b default is passed through py::arg_v with an explicit numpy.<type>(...) repr that stubgen evaluates and emits as an elided ..., valid against any annotation, for two reasons:

    • the numpy_scalar overloads — otherwise pybind11 formats the default from numpy's own repr (np.<type>(...) under numpy 2.x), an unimported np alias that mypy rejects;
    • the cytnx_complex128 overload — pybind11 3.0.4's SupportsComplex | SupportsFloat | SupportsIndex annotation excludes the builtin complex (typeshed's complex has no __complex__), so a literal 0j default would be flagged as an incompatible default.

    The defaults still exist at runtime, so stubtest is unaffected and tools/generate_stubs.py needs no special-casing.

Numerical note / issue #914

linalg.Exp returns incorrect values for complex64 (ComplexFloat) tensors, which both ExpH and ExpM depend 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.pyi is mypy-clean, but stubtest still cannot run a clean comparison: ~5599 overload-cannot-match errors 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 to ExpH/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/ExpM accept 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.pyi is mypy-clean: python -m mypy.stubtest cytnx.cytnx reports zero errors for it, and regenerating the stubs is idempotent (the arg_v defaults reproduce the committed .pyi byte-for-byte).
  • C++ gtest suites were not rebuilt: this PR changes only pybind glue; the C++ ExpH/ExpM kernels are untouched.

🤖 Generated with Claude Code

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

Copy link
Copy Markdown

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 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.

Comment thread cytnx/Bond_conti.py
Comment thread pybind/scalar_py.cpp Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread pybind/linalg_py.cpp Outdated
@IvanaGyro IvanaGyro marked this pull request as draft June 18, 2026 16:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread cytnx/cytnx/linalg.pyi Outdated
@IvanaGyro

Copy link
Copy Markdown
Member Author

I didn't really know what mypy's stubtest tests and tlthe best solution either. This PR may not correct.

@IvanaGyro IvanaGyro force-pushed the claude/fix-stubtest-errors branch from 94d54c9 to bb32d2d Compare June 19, 2026 19:30
@IvanaGyro IvanaGyro force-pushed the claude/pybind11-stubgen-stubs-ebny1c branch from c7473d7 to 6700547 Compare June 20, 2026 01:20
@IvanaGyro IvanaGyro force-pushed the claude/fix-stubtest-errors branch from bb32d2d to 5bd01bb Compare June 20, 2026 01:20
@IvanaGyro IvanaGyro changed the title Bind ExpH/ExpM with one Scalar overload + Python-layer fixes (toward clean stubtest) Bind ExpH/ExpM with per-dtype overloads + Python-layer fixes (toward clean stubtest) Jun 20, 2026

Copy link
Copy Markdown
Member Author

Heads-up from running the full pytest pytests/ on the rebased stack — not caused by this PR, flagging for awareness:

pytests/example/TDVP/tdvp1_dense_test.py::test_tdvp1_dense fails its tolerance:

error = np.abs(Es[-1] - (-1.0 * Nsites)) = 1.72e-4
assert error < 1e-6

This is unrelated to the changes here: TDVP drives time evolution through linalg.Lanczos_Exp, not the ExpH/ExpM bindings this PR touches, and the test passed before rebasing onto current master. master reworked Lanczos_Exp since this branch's fork point (#889 "Fix TDVP Lanczos_Exp instability", #892 "Fix Lanczos_Exp warning at full Krylov dimension", and "Fix Lanczos_Exp one-dimensional Krylov spaces"), which governs this result. Worth tracking separately on master — in particular whether the 1e-6 tolerance is still appropriate after those changes.

— Claude


Generated by Claude Code

@IvanaGyro IvanaGyro force-pushed the claude/pybind11-stubgen-stubs-ebny1c branch from 6700547 to bffc010 Compare June 20, 2026 03:30
@IvanaGyro IvanaGyro force-pushed the claude/fix-stubtest-errors branch 2 times, most recently from 25a5894 to 55031b7 Compare June 20, 2026 06:04
@IvanaGyro IvanaGyro force-pushed the claude/pybind11-stubgen-stubs-ebny1c branch from bffc010 to c1e1f45 Compare June 20, 2026 06:12
@IvanaGyro IvanaGyro force-pushed the claude/fix-stubtest-errors branch from 55031b7 to f182481 Compare June 20, 2026 06:16
@IvanaGyro IvanaGyro marked this pull request as ready for review June 20, 2026 07:07
@IvanaGyro IvanaGyro marked this pull request as draft June 20, 2026 07:07
@ianmccul

Copy link
Copy Markdown
Contributor

Heads-up from running the full pytest pytests/ on the rebased stack — not caused by this PR, flagging for awareness:

pytests/example/TDVP/tdvp1_dense_test.py::test_tdvp1_dense fails its tolerance:

error = np.abs(Es[-1] - (-1.0 * Nsites)) = 1.72e-4
assert error < 1e-6

This is unrelated to the changes here: TDVP drives time evolution through linalg.Lanczos_Exp, not the ExpH/ExpM bindings this PR touches, and the test passed before rebasing onto current master. master reworked Lanczos_Exp since this branch's fork point (#889 "Fix TDVP Lanczos_Exp instability", #892 "Fix Lanczos_Exp warning at full Krylov dimension", and "Fix Lanczos_Exp one-dimensional Krylov spaces"), which governs this result. Worth tracking separately on master — in particular whether the 1e-6 tolerance is still appropriate after those changes.

— Claude

Generated by Claude Code

@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 1e-6 tolerance here is far too weak: for a long time it was allowing implementation bugs in Lanczos_Exp and related functions to go unnoticed. Almost certainly, it means the branch being tested is following a numerically different path or missing a relevant fix.

@ 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.

@ianmccul

Copy link
Copy Markdown
Contributor

If the goal is to bind linalg.ExpH / linalg.ExpM with one overload per Python-distinguishable dtype, the cleaner solution is to expose one C++ overload per dtype as well, most naturally through templated C++ entry points.

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 Exp / Expf how easy it is for these runtime dtype-dispatch paths to become type-unsafe: the binding/API surface appears dtype-specific, but the implementation still passes untyped storage through a dispatch table and relies on the selected runtime dtype matching the actual storage.

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.

@IvanaGyro IvanaGyro force-pushed the claude/pybind11-stubgen-stubs-ebny1c branch from 725a8dd to 37e3210 Compare June 21, 2026 17:22
@IvanaGyro IvanaGyro force-pushed the claude/fix-stubtest-errors branch 3 times, most recently from 6832f72 to bab016c Compare June 21, 2026 19:02
@IvanaGyro IvanaGyro force-pushed the claude/pybind11-stubgen-stubs-ebny1c branch from 37e3210 to 9e7e777 Compare June 21, 2026 19:33
@IvanaGyro IvanaGyro force-pushed the claude/fix-stubtest-errors branch from bab016c to 3088d86 Compare June 21, 2026 19:38
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>
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.

2 participants