fix(bindings): mp complex → real numpy cast discards imag, not SIGABRT (3.3.1.dev1) - #336
Merged
ofloveandhate merged 2 commits intoJul 13, 2026
Conversation
Storing a complex_mp with a nonzero imaginary part into a real (float64)
numpy array hard-crashed the interpreter: the registered complex_mp -> double
cast did static_cast<To>(from), which routes through boost.multiprecision's
complex->scalar conversion and THROWS "Could not convert imaginary number to
scalar." for any nonzero imaginary part. Thrown out of numpy's C cast loop
(an implicitly-noexcept boundary), that reaches std::terminate() -> SIGABRT
rather than a catchable Python error. It bites naturally, e.g.
M = np.zeros(...); M[i, j] = solver.real_solutions()[k][0]
since solution coordinates carry ~1e-13 imaginary noise and eval returns a
complex_mp. Convert the real component instead, mirroring numpy's builtin
complex128 -> float64 cast (which discards the imaginary part). This is the
only real-target cast registered for complex_mp (-> complex128 has its own
specialization; -> integer is unregistered).
Also: the eigenpy_numpy numpy-interop suite was never collected by pytest
(filename matched neither test_*.py nor *_test.py) -- renamed to
eigenpy_numpy_test.py so it runs, plus regression tests for the mp ->
narrower-dtype casts (scalar setitem, .astype, broadcast, numpy-builtin
parity, and the complex128 no-regression check). ADR-0055.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes a hard interpreter crash (SIGABRT) when a multiprecision complex value with a
nonzero imaginary part is stored into a real (
float64) numpy array — and ships it as3.3.1.dev1.Why it crashed
The registered
complex_mp → doublenumpy cast didstatic_cast<To>(from), which routesthrough boost.multiprecision's complex→scalar conversion. That throws
std::runtime_error("Could not convert imaginary number to scalar.")for any nonzeroimaginary part. numpy invokes the cast from an internal C loop that is not exception-safe, so
the C++ throw unwinds to an implicitly-
noexceptboundary →std::terminate()→ SIGABRT,instead of a catchable Python error. Solution coordinates always carry tiny (~1e-13)
imaginary noise and
evalreturns acomplex_mp, so this is trivially hit (it surfacedbuilding a Macaulay matrix for a local-dimension test).
Fix
The
complex_mp → realcast now converts the real component, mirroring numpy's builtincomplex128 → float64cast (which discards the imaginary part). One line incast<complex_mp, To>(python_bindings/include/eigenpy_interaction.hpp). This is the onlyreal-target cast registered for
complex_mp—→ complex128has its own specialization(preserves both parts) and
→ integeris unregistered — so nothing else moves. ADR-0055records the guardrail (don't restore the
static_castform).Tests
The
eigenpy_numpynumpy-interop suite was never collected by pytest (its filenamematched neither
test_*.pynor*_test.py) — renamed toeigenpy_numpy_test.pyso its 15existing tests actually run in CI, plus 7 new regression tests for the mp → narrower-dtype
casts: scalar setitem (the exact crash), vectorized
.astype, broadcast assignment, paritywith the numpy builtin
complex128 → float64, and acomplex_mp → complex128no-regressioncheck. Full
python/test/classessuite green (571 passed).Release
Second commit is the prep:
CHANGELOG [3.3.1]+VERSION → 3.3.1.dev1. Once merged I'll tagv3.3.1.dev1(→ TestPyPI); after that's green,v3.3.1for real.🤖 Generated with Claude Code