Skip to content

Commit 2cdaf5b

Browse files
dwpaleybkpoon
authored andcommitted
Register Boost.Python converters for NumPy 2.0 scalar types
Workaround for boostorg/python#511 NumPy 2.0 scalar types (np.float32, np.int32, np.int64, etc.) no longer subclass Python's built-in float/int, so Boost.Python's rvalue converters fail to convert them. Register custom from-Python converters that use the PyNumber_Float/PyNumber_Long protocols instead. Fixes #1084
1 parent c657b13 commit 2cdaf5b

5 files changed

Lines changed: 216 additions & 0 deletions

File tree

scitbx/array_family/boost_python/flex_ext.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ namespace scitbx { namespace af { namespace boost_python {
3838
#else
3939
void import_numpy_api_if_available();
4040
#endif
41+
void register_numpy_scalar_converters();
4142
void wrap_flex_grid();
4243
void wrap_flex_bool();
4344
void wrap_flex_size_t();
@@ -464,6 +465,7 @@ namespace {
464465
using boost::python::arg;
465466

466467
import_numpy_api_if_available();
468+
register_numpy_scalar_converters();
467469

468470
register_scitbx_tuple_mappings();
469471

scitbx/array_family/boost_python/numpy_bridge.cpp

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include <stdint.h>
22
#include <boost/python/numpy.hpp>
33
#include <boost/python.hpp>
4+
#include <boost/python/converter/registry.hpp>
45
#include <scitbx/array_family/versa.h>
56
#include <scitbx/array_family/accessors/flex_grid.h>
67
#include <boost_adaptbx/type_id_eq.h>
@@ -230,4 +231,105 @@ namespace scitbx { namespace af { namespace boost_python {
230231

231232
#undef SCITBX_LOC
232233

234+
// Workaround for https://github.com/boostorg/python/issues/511
235+
// NumPy 2.0 scalar types (e.g. np.float32, np.int32) no longer subclass
236+
// Python's built-in float/int, so Boost.Python's rvalue converters
237+
// (which use PyFloat_Check/PyLong_Check) fail to convert them.
238+
// Register custom converters that use PyNumber_Float/PyNumber_Long instead.
239+
240+
namespace {
241+
242+
// Converter for numpy scalar -> C++ floating point types
243+
template <typename CppType>
244+
struct numpy_scalar_to_floating {
245+
static void* convertible(PyObject* obj) {
246+
#if defined(SCITBX_HAVE_NUMPY_INCLUDE)
247+
if (PyArray_IsScalar(obj, Number) && !PyArray_IsScalar(obj, ComplexFloating)) {
248+
return obj;
249+
}
250+
#endif
251+
return nullptr;
252+
}
253+
254+
static void construct(
255+
PyObject* obj,
256+
boost::python::converter::rvalue_from_python_stage1_data* data)
257+
{
258+
void* storage = reinterpret_cast<
259+
boost::python::converter::rvalue_from_python_storage<CppType>*>(
260+
data)->storage.bytes;
261+
PyObject* as_float = PyNumber_Float(obj);
262+
if (!as_float) boost::python::throw_error_already_set();
263+
CppType value = static_cast<CppType>(PyFloat_AsDouble(as_float));
264+
if (value == static_cast<CppType>(-1.0) && PyErr_Occurred()) {
265+
Py_DECREF(as_float);
266+
boost::python::throw_error_already_set();
267+
}
268+
Py_DECREF(as_float);
269+
new (storage) CppType(value);
270+
data->convertible = storage;
271+
}
272+
};
273+
274+
// Converter for numpy scalar -> C++ integer types
275+
template <typename CppType>
276+
struct numpy_scalar_to_integer {
277+
static void* convertible(PyObject* obj) {
278+
#if defined(SCITBX_HAVE_NUMPY_INCLUDE)
279+
if (PyArray_IsScalar(obj, Integer)) {
280+
return obj;
281+
}
282+
#endif
283+
return nullptr;
284+
}
285+
286+
static void construct(
287+
PyObject* obj,
288+
boost::python::converter::rvalue_from_python_stage1_data* data)
289+
{
290+
void* storage = reinterpret_cast<
291+
boost::python::converter::rvalue_from_python_storage<CppType>*>(
292+
data)->storage.bytes;
293+
PyObject* as_long = PyNumber_Long(obj);
294+
if (!as_long) boost::python::throw_error_already_set();
295+
CppType value = static_cast<CppType>(PyLong_AsLong(as_long));
296+
if (value == static_cast<CppType>(-1) && PyErr_Occurred()) {
297+
Py_DECREF(as_long);
298+
boost::python::throw_error_already_set();
299+
}
300+
Py_DECREF(as_long);
301+
new (storage) CppType(value);
302+
data->convertible = storage;
303+
}
304+
};
305+
306+
} // anonymous namespace
307+
308+
void register_numpy_scalar_converters()
309+
{
310+
#if defined(SCITBX_HAVE_NUMPY_INCLUDE)
311+
using namespace boost::python;
312+
313+
// Floating point converters
314+
converter::registry::push_back(
315+
&numpy_scalar_to_floating<double>::convertible,
316+
&numpy_scalar_to_floating<double>::construct,
317+
type_id<double>());
318+
converter::registry::push_back(
319+
&numpy_scalar_to_floating<float>::convertible,
320+
&numpy_scalar_to_floating<float>::construct,
321+
type_id<float>());
322+
323+
// Integer converters
324+
converter::registry::push_back(
325+
&numpy_scalar_to_integer<int>::convertible,
326+
&numpy_scalar_to_integer<int>::construct,
327+
type_id<int>());
328+
converter::registry::push_back(
329+
&numpy_scalar_to_integer<long>::convertible,
330+
&numpy_scalar_to_integer<long>::construct,
331+
type_id<long>());
332+
#endif
333+
}
334+
233335
}}} // namespace scitbx::af::boost_python

scitbx/array_family/boost_python/numpy_bridge.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,6 @@ namespace scitbx { namespace af { namespace boost_python {
3434
// SCITBX_LOC(uint64, uint64_t)
3535
#undef SCITBX_LOC
3636

37+
void register_numpy_scalar_converters();
38+
3739
}}} // namespace scitbx::af::boost_python
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from __future__ import absolute_import, division, print_function
2+
import sys
3+
4+
def run(args):
5+
assert len(args) == 0
6+
try:
7+
import numpy as np
8+
except ImportError:
9+
print("numpy not available, skipping")
10+
print("OK")
11+
return
12+
from scitbx.array_family import flex
13+
14+
exercise_original_reproducer(np, flex)
15+
exercise_element_setitem(np, flex)
16+
exercise_element_iadd(np, flex)
17+
exercise_array_iadd(np, flex)
18+
exercise_construction_from_numpy_scalars(np, flex)
19+
exercise_value_preservation(np, flex)
20+
print("OK")
21+
22+
def exercise_original_reproducer(np, flex):
23+
"""Reproducer from https://github.com/cctbx/cctbx_project/issues/1084"""
24+
arr = flex.double(10)
25+
arr[0] += np.array([1,2,3], dtype=np.float32)[0]
26+
assert arr[0] == 1.0
27+
28+
def exercise_element_setitem(np, flex):
29+
"""Test arr[i] = numpy_scalar for various type combinations."""
30+
# float scalars -> flex.double
31+
a = flex.double(1)
32+
for val in [np.float32(3.5), np.float64(3.5)]:
33+
a[0] = val
34+
assert a[0] == 3.5, (a[0], type(val))
35+
# integer scalars -> flex.double (implicit widening)
36+
for val in [np.int32(7), np.int64(7)]:
37+
a[0] = val
38+
assert a[0] == 7.0, (a[0], type(val))
39+
# float scalars -> flex.float
40+
b = flex.float(1)
41+
for val in [np.float32(2.5), np.float64(2.5)]:
42+
b[0] = val
43+
assert abs(b[0] - 2.5) < 1e-6, (b[0], type(val))
44+
# integer scalars -> flex.int
45+
c = flex.int(1)
46+
for val in [np.int32(42), np.int64(42)]:
47+
c[0] = val
48+
assert c[0] == 42, (c[0], type(val))
49+
50+
def exercise_element_iadd(np, flex):
51+
"""Test arr[i] += numpy_scalar for various type combinations."""
52+
a = flex.double([10.0])
53+
a[0] += np.float32(2.5)
54+
assert a[0] == 12.5
55+
a[0] += np.float64(1.0)
56+
assert a[0] == 13.5
57+
a[0] += np.int32(1)
58+
assert a[0] == 14.5
59+
a[0] += np.int64(1)
60+
assert a[0] == 15.5
61+
62+
b = flex.int([10])
63+
b[0] += np.int32(5)
64+
assert b[0] == 15
65+
b[0] += np.int64(3)
66+
assert b[0] == 18
67+
68+
def exercise_array_iadd(np, flex):
69+
"""Test arr += numpy_scalar (whole-array operations)."""
70+
a = flex.double([1.0, 2.0, 3.0])
71+
a += np.float32(10.0)
72+
assert list(a) == [11.0, 12.0, 13.0]
73+
a += np.float64(1.0)
74+
assert list(a) == [12.0, 13.0, 14.0]
75+
a += np.int32(1)
76+
assert list(a) == [13.0, 14.0, 15.0]
77+
78+
b = flex.int([1, 2, 3])
79+
b += np.int32(10)
80+
assert list(b) == [11, 12, 13]
81+
b += np.int64(1)
82+
assert list(b) == [12, 13, 14]
83+
84+
def exercise_construction_from_numpy_scalars(np, flex):
85+
"""Test constructing flex arrays from lists containing numpy scalars."""
86+
a = flex.double([np.float32(1.0), np.float64(2.0), np.float32(3.0)])
87+
assert list(a) == [1.0, 2.0, 3.0]
88+
b = flex.int([np.int32(1), np.int32(2), np.int32(3)])
89+
assert list(b) == [1, 2, 3]
90+
91+
def exercise_value_preservation(np, flex):
92+
"""Test that values are preserved accurately through conversion."""
93+
# float32 has ~7 decimal digits of precision
94+
a = flex.double(1)
95+
a[0] = np.float32(1.23456789)
96+
# float32 truncates, so check against float32 precision
97+
assert abs(a[0] - float(np.float32(1.23456789))) < 1e-10
98+
99+
# float64 should be exact for representable values
100+
a[0] = np.float64(1.234567890123456)
101+
assert a[0] == 1.234567890123456
102+
103+
# Large integers
104+
b = flex.int(1)
105+
b[0] = np.int32(2147483647) # INT32_MAX
106+
assert b[0] == 2147483647
107+
108+
if __name__ == "__main__":
109+
run(args=sys.argv[1:])

scitbx/run_tests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"$D/array_family/boost_python/regression_test.py",
4545
"$D/array_family/boost_python/tst_flex.py",
4646
"$D/array_family/boost_python/tst_numpy_bridge.py",
47+
"$D/array_family/boost_python/tst_numpy_scalar_conversions.py",
4748
"$D/array_family/boost_python/tst_smart_selection.py",
4849
"$D/array_family/boost_python/tst_shared.py",
4950
"$D/array_family/boost_python/tst_integer_offsets_vs_pointers.py",

0 commit comments

Comments
 (0)