โš ๏ธ Known gotchasยถ

A few sharp edges fall out of how Bertini 2โ€™s multiprecision numbers are exposed to NumPy. They are collected here with the idiom that works and the idiom that bites.

NumPy reductions over multiprecision arraysยถ

Bertini 2 exposes real_mp (variable-precision real) and complex_mp (variable-precision complex) as custom NumPy dtypes (via eigenpy). Element-wise math, indexing, @ / numpy.dot(), numpy.linalg.norm(), numpy.cumsum() and friends all work on arrays of these dtypes.

The sharp edge is the identity-seeded reductions โ€“ numpy.sum(), numpy.prod(), numpy.mean(), and a bare ufunc.reduce with no initial=:

>>> import numpy as np
>>> from bertini.multiprec import complex_mp
>>> v = np.array([complex_mp(3), complex_mp(4)], dtype=complex_mp)
>>> np.sum(v)                                    # โœ— DON'T -- may crash
Traceback (most recent call last):
    ...
SystemError: <method 'reduce' of 'numpy.ufunc' objects> returned NULL without setting an exception

Why this happensยถ

To reduce an array, NumPy needs the reductionโ€™s identity element โ€“ 0 for a sum, 1 for a product โ€“ materialized in the arrayโ€™s dtype. For these custom dtypes, NumPy (2.x) tries to build that identity from a Python int and fails inside its own machinery, before ever calling Bertiniโ€™s dtype code, returning NULL without setting a Python exception (hence the bare SystemError). It is a limitation of the NumPy โ†” eigenpy boundary for legacy user-defined dtypes, not something Bertini can patch in its bindings, and whether it triggers depends on the exact NumPy / eigenpy / Eigen build โ€“ so it may โ€œworkโ€ on one machine and crash on another. Treat it as always-unsupported.

What to do insteadยถ

Give the reduction a value to start from, or use an operation that seeds itself from the data (dot / norm / a plain Python loop). All of these are stable across builds:

>>> import numpy as np
>>> from bertini.multiprec import real_mp, complex_mp
>>> v = np.array([complex_mp(3), complex_mp(4)], dtype=complex_mp)
>>> w = np.array([real_mp(1), real_mp(2), real_mp(3)], dtype=real_mp)

>>> # โœ“ sum: hand ufunc.reduce an explicit, correctly-typed identity
>>> complex(np.add.reduce(v, initial=complex_mp(0)))
(7+0j)
>>> float(np.add.reduce(w, initial=real_mp(0)))
6.0

>>> # โœ“ sum: or just use Python's built-in sum()
>>> float(sum(w))
6.0

>>> # โœ“ Euclidean norm works directly (it routes through the dtype's dot slot)
>>> float(np.linalg.norm(v))
5.0

>>> # โœ“ sum of squares: np.dot does not conjugate, so dot(v, v) == sum(v**2)
>>> complex(np.dot(v, v))
(25+0j)

>>> # โœ“ mean: reduce with an identity, then divide by the count
>>> float(np.add.reduce(w, initial=real_mp(0)) / w.size)
2.0

In short: anywhere you would reach for np.sum(a) / np.prod(a) / np.mean(a) on a real_mp or complex_mp array, reach for np.add.reduce(a, initial=...) (or np.multiply.reduce(a, initial=...)), np.dot / np.linalg.norm, or a plain Python sum instead.