Skip to content

Commit 8d21103

Browse files
committed
[cppyy] Add test for C++ array NumPy views
1 parent 6146db4 commit 8d21103

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed

bindings/pyroot/pythonizations/test/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,10 @@ if (NOT MSVC AND tmva)
197197
endif()
198198

199199
ROOT_ADD_PYUNITTEST(regression_18441 regression_18441.py)
200+
201+
if(NOT MSVC OR CMAKE_SIZEOF_VOID_P EQUAL 8 OR win_broken_tests)
202+
# Test the conversion and low level views of interpreter-defined C++ arrays to NumPy
203+
# We do not run this test on Windows x86 due to an interpreter issue with arrays:
204+
# https://github.com/root-project/root/issues/9809
205+
ROOT_ADD_PYUNITTEST(pyroot_array_numpy_views array_conversions.py PYTHON_DEPS numpy)
206+
endif()
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import unittest
2+
3+
import numpy as np
4+
import ROOT
5+
6+
7+
class NumpyArrayView(unittest.TestCase):
8+
"""
9+
Test the conversion of interpreter-defined C++ arrays into numpy views
10+
"""
11+
12+
# typemaps based on https://numpy.org/doc/stable/reference/arrays.scalars.html
13+
cpp_dtypes = [
14+
"char",
15+
"unsigned char",
16+
"int",
17+
"unsigned int",
18+
"short",
19+
"unsigned short",
20+
"float",
21+
"int8_t",
22+
"uint8_t",
23+
"int16_t",
24+
"uint16_t",
25+
"int32_t",
26+
"uint32_t",
27+
]
28+
29+
np_dtypes = [
30+
np.byte,
31+
np.ubyte,
32+
np.intc,
33+
np.uintc,
34+
np.short,
35+
np.ushort,
36+
np.float32,
37+
np.int8,
38+
np.uint8,
39+
np.int16,
40+
np.uint16,
41+
np.int32,
42+
np.uint32,
43+
]
44+
45+
typemap = zip(np_dtypes, cpp_dtypes)
46+
47+
bounds = {
48+
"char": (-128, 127),
49+
"unsigned char": (0, 255),
50+
"int": (-(2**31), 2**31 - 1),
51+
"unsigned int": (0, 2**32 - 1),
52+
"short": (-(2**15), 2**15 - 1),
53+
"unsigned short": (0, 2**16 - 1),
54+
# FIXME : low level views for 64 bit types (long and double) do not work, upstream interprets the converter with dims = 0,
55+
# which somehow makes this work, however this needs to be investigated further.
56+
"long": (-(2**31), 2**31 - 1),
57+
"long long": (-(2**62), 2**62 - 1),
58+
"unsigned long": (0, 2**32 - 1),
59+
"unsigned long long": (0, 2**64 - 1),
60+
"float": (-3.4e38, 3.4e38),
61+
"double": (-1.7e308, 1.7e308),
62+
"int8_t": (-128, 127),
63+
"uint8_t": (0, 255),
64+
"int16_t": (-(2**15), 2**15 - 1),
65+
"uint16_t": (0, 2**16 - 1),
66+
"int32_t": (-(2**31), 2**31 - 1),
67+
"uint32_t": (0, 2**32 - 1),
68+
}
69+
70+
def generate_cpp_arrays(self, dtype_cpp):
71+
mn, mx = self.bounds[dtype_cpp]
72+
# sanitize a name for the struct so there's no spaces
73+
tag = dtype_cpp.replace(" ", "_").replace("unsigned_", "u")
74+
75+
cpp = f"""
76+
struct Foo_{tag} {{
77+
{dtype_cpp} bar[11][2] = {{}};
78+
}};
79+
Foo_{tag} foo_{tag};
80+
foo_{tag}.bar[0][0] = {mn};
81+
foo_{tag}.bar[1][1] = {mx};
82+
foo_{tag}.bar[2][0] = {mn};
83+
foo_{tag}.bar[3][1] = {mx};
84+
foo_{tag}.bar[4][0] = {mn};
85+
foo_{tag}.bar[5][1] = {mx};
86+
foo_{tag}.bar[6][0] = {mn};
87+
foo_{tag}.bar[7][1] = {mx};
88+
foo_{tag}.bar[8][0] = {mn};
89+
foo_{tag}.bar[9][1] = {mx};
90+
foo_{tag}.bar[10][0] = {mn};
91+
"""
92+
ROOT.gInterpreter.ProcessLine(cpp)
93+
return getattr(ROOT, f"foo_{tag}").bar
94+
95+
def check_shape(self, cpp_arr, np_obj):
96+
self.assertEqual(cpp_arr.shape, np_obj.shape)
97+
98+
def validate_numpy_view(self, np_obj, dtype):
99+
# obtain bounds for C++ builtins
100+
mn, mx = self.bounds[dtype[1]]
101+
kind = dtype[0]
102+
103+
if issubclass(kind, np.integer):
104+
cast = int
105+
elif issubclass(kind, np.floating):
106+
107+
def cast(v):
108+
return float(f"{v:.2e}")
109+
110+
for i, row in enumerate(np_obj[:11]):
111+
# we check col 0 for even i, 1 for odd i, as the array was filled that way
112+
col = i & 1
113+
val = cast(row[col])
114+
115+
# the expected bound is min for col 0 and max for col 1
116+
expected = mn if col == 0 else mx
117+
self.assertEqual(val, expected)
118+
119+
def test_2DArray_NumpyView(self):
120+
"""
121+
Test correct numpy view for different C++ builtin-type 2D arrays
122+
"""
123+
for dtype in self.typemap:
124+
cpp_arr = self.generate_cpp_arrays(dtype[1])
125+
np_obj = np.frombuffer(cpp_arr, dtype[0], count=11 * 2).reshape(11, 2)
126+
self.check_shape(cpp_arr, np_obj)
127+
self.validate_numpy_view(np_obj, dtype)
128+
129+
130+
if __name__ == "__main__":
131+
unittest.main()

0 commit comments

Comments
 (0)