Skip to content

fix(bindings): mp complex → real numpy cast discards imag, not SIGABRT (3.3.1.dev1) - #336

Merged
ofloveandhate merged 2 commits into
bertiniteam:developfrom
ofloveandhate:fix/complex-mp-real-numpy-cast
Jul 13, 2026
Merged

fix(bindings): mp complex → real numpy cast discards imag, not SIGABRT (3.3.1.dev1)#336
ofloveandhate merged 2 commits into
bertiniteam:developfrom
ofloveandhate:fix/complex-mp-real-numpy-cast

Conversation

@ofloveandhate

Copy link
Copy Markdown
Contributor

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 as
3.3.1.dev1.

M = np.zeros((r, c))                       # float64
M[i, j] = solver.real_solutions()[k][0]    # complex_mp w/ ~1e-13 imag  → SIGABRT (pre-fix)

Why it crashed

The registered complex_mp → double numpy cast did static_cast<To>(from), which routes
through boost.multiprecision's complex→scalar conversion. That throws
std::runtime_error("Could not convert imaginary number to scalar.") for any nonzero
imaginary part. numpy invokes the cast from an internal C loop that is not exception-safe, so
the C++ throw unwinds to an implicitly-noexcept boundary → std::terminate() → SIGABRT,
instead of a catchable Python error. Solution coordinates always carry tiny (~1e-13)
imaginary noise and eval returns a complex_mp, so this is trivially hit (it surfaced
building a Macaulay matrix for a local-dimension test).

Fix

The complex_mp → real cast now converts the real component, mirroring numpy's builtin
complex128 → float64 cast (which discards the imaginary part). One line in
cast<complex_mp, To> (python_bindings/include/eigenpy_interaction.hpp). This is the only
real-target cast registered for complex_mp→ complex128 has its own specialization
(preserves both parts) and → integer is unregistered — so nothing else moves. ADR-0055
records the guardrail (don't restore the static_cast form).

Tests

The eigenpy_numpy numpy-interop suite was never collected by pytest (its filename
matched neither test_*.py nor *_test.py) — renamed to eigenpy_numpy_test.py so its 15
existing 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, parity
with the numpy builtin complex128 → float64, and a complex_mp → complex128 no-regression
check. Full python/test/classes suite green (571 passed).

Release

Second commit is the prep: CHANGELOG [3.3.1] + VERSION → 3.3.1.dev1. Once merged I'll tag
v3.3.1.dev1 (→ TestPyPI); after that's green, v3.3.1 for real.

🤖 Generated with Claude Code

ofloveandhate and others added 2 commits July 12, 2026 20:32
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>
@ofloveandhate
ofloveandhate merged commit d070dbd into bertiniteam:develop Jul 13, 2026
33 checks passed
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