Skip to content

Commit fae6fa4

Browse files
authored
ENH: Adding scalar.bit_count() (popcount) (numpy#19355)
Adding bitcount method to scalars, e.g.: a = np.int32(1023).bit_count() * ENH: Implementation of bit_count (popcount) * ENH: Add bit_count to integer scalar type * ENH: Annotations for bit_count * ENH, WIP: Documentation for bit_count * DOC: Added `bit_count` (numpy#19355) * BUG: Fixed windows 32 bit issue with no `__popcnt64` * DOC: Refined docstring for bit_count * TST: Tests for bit_count * ENH, MAINT: Changed return type to uint_8 | Removed extra braces and fixed typo * BUG: Fixed syntax of bit_count * DOC, BUG: Fixed bit_count example * DOC, BUG: (numpy#19355) Removed bit_count from routines.math.rst | Improved release notes * BUG: Added type suffix to magic constants * ENH: Handle 32 bit windows popcount | Refactored popcount implementation to new function * MAINT: Refactor type_methods, separate integer definitions * DOC: Added double-ticks
1 parent 987d25b commit fae6fa4

File tree

7 files changed

+198
-2
lines changed

7 files changed

+198
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
``bit_count`` to compute the number of 1-bits in an integer
2+
-----------------------------------------------------------
3+
4+
Computes the number of 1-bits in the absolute value of the input.
5+
This works on all the numpy integer types. Analogous to the builtin
6+
``int.bit_count`` or ``popcount`` in C++.
7+
8+
.. code-block:: python
9+
10+
>>> np.uint32(1023).bit_count()
11+
10
12+
>>> np.int32(-127).bit_count()
13+
7

numpy/__init__.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -2750,6 +2750,7 @@ class integer(number[_NBit1]): # type: ignore
27502750
) -> int: ...
27512751
def tolist(self) -> int: ...
27522752
def is_integer(self) -> L[True]: ...
2753+
def bit_count(self: _ScalarType) -> int: ...
27532754
def __index__(self) -> int: ...
27542755
__truediv__: _IntTrueDiv[_NBit1]
27552756
__rtruediv__: _IntTrueDiv[_NBit1]

numpy/core/_add_newdocs_scalars.py

+19
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,22 @@ def add_newdoc_for_scalar_type(obj, fixed_aliases, doc):
290290
>>> np.{float_name}(3.2).is_integer()
291291
False
292292
"""))
293+
294+
for int_name in ('int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32',
295+
'int64', 'uint64', 'int64', 'uint64', 'int64', 'uint64'):
296+
# Add negative examples for signed cases by checking typecode
297+
add_newdoc('numpy.core.numerictypes', int_name, ('bit_count',
298+
f"""
299+
{int_name}.bit_count() -> int
300+
301+
Computes the number of 1-bits in the absolute value of the input.
302+
Analogous to the builtin `int.bit_count` or ``popcount`` in C++.
303+
304+
Examples
305+
--------
306+
>>> np.{int_name}(127).bit_count()
307+
7""" +
308+
(f"""
309+
>>> np.{int_name}(-127).bit_count()
310+
7
311+
""" if dtype(int_name).char.islower() else "")))

numpy/core/include/numpy/npy_math.h

+11
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,17 @@ NPY_INPLACE npy_long npy_lshiftl(npy_long a, npy_long b);
150150
NPY_INPLACE npy_longlong npy_rshiftll(npy_longlong a, npy_longlong b);
151151
NPY_INPLACE npy_longlong npy_lshiftll(npy_longlong a, npy_longlong b);
152152

153+
NPY_INPLACE uint8_t npy_popcountuhh(npy_ubyte a);
154+
NPY_INPLACE uint8_t npy_popcountuh(npy_ushort a);
155+
NPY_INPLACE uint8_t npy_popcountu(npy_uint a);
156+
NPY_INPLACE uint8_t npy_popcountul(npy_ulong a);
157+
NPY_INPLACE uint8_t npy_popcountull(npy_ulonglong a);
158+
NPY_INPLACE uint8_t npy_popcounthh(npy_byte a);
159+
NPY_INPLACE uint8_t npy_popcounth(npy_short a);
160+
NPY_INPLACE uint8_t npy_popcount(npy_int a);
161+
NPY_INPLACE uint8_t npy_popcountl(npy_long a);
162+
NPY_INPLACE uint8_t npy_popcountll(npy_longlong a);
163+
153164
/*
154165
* C99 double math funcs
155166
*/

numpy/core/src/multiarray/scalartypes.c.src

+50-2
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,27 @@ gentype_multiply(PyObject *m1, PyObject *m2)
218218
return PyArray_Type.tp_as_number->nb_multiply(m1, m2);
219219
}
220220

221+
/**begin repeat
222+
* #TYPE = BYTE, UBYTE, SHORT, USHORT, INT, UINT,
223+
* LONG, ULONG, LONGLONG, ULONGLONG#
224+
* #type = npy_byte, npy_ubyte, npy_short, npy_ushort, npy_int, npy_uint,
225+
* npy_long, npy_ulong, npy_longlong, npy_ulonglong#
226+
* #c = hh, uhh, h, uh,, u, l, ul, ll, ull#
227+
* #Name = Byte, UByte, Short, UShort, Int, UInt,
228+
* Long, ULong, LongLong, ULongLong#
229+
* #convert = Long*8, LongLong*2#
230+
*/
231+
static PyObject *
232+
@type@_bit_count(PyObject *self)
233+
{
234+
@type@ scalar = PyArrayScalar_VAL(self, @Name@);
235+
uint8_t count = npy_popcount@c@(scalar);
236+
PyObject *result = PyLong_From@convert@(count);
237+
238+
return result;
239+
}
240+
/**end repeat**/
241+
221242
/**begin repeat
222243
*
223244
* #name = positive, negative, absolute, invert, int, float#
@@ -2316,8 +2337,7 @@ static PyMethodDef @name@type_methods[] = {
23162337
/**end repeat**/
23172338

23182339
/**begin repeat
2319-
* #name = byte, short, int, long, longlong, ubyte, ushort,
2320-
* uint, ulong, ulonglong, timedelta, cdouble#
2340+
* #name = timedelta, cdouble#
23212341
*/
23222342
static PyMethodDef @name@type_methods[] = {
23232343
/* for typing; requires python >= 3.9 */
@@ -2328,6 +2348,23 @@ static PyMethodDef @name@type_methods[] = {
23282348
};
23292349
/**end repeat**/
23302350

2351+
/**begin repeat
2352+
* #name = byte, ubyte, short, ushort, int, uint,
2353+
* long, ulong, longlong, ulonglong#
2354+
*/
2355+
static PyMethodDef @name@type_methods[] = {
2356+
/* for typing; requires python >= 3.9 */
2357+
{"__class_getitem__",
2358+
(PyCFunction)numbertype_class_getitem,
2359+
METH_CLASS | METH_O, NULL},
2360+
{"bit_count",
2361+
(PyCFunction)npy_@name@_bit_count,
2362+
METH_NOARGS, NULL},
2363+
{NULL, NULL, 0, NULL} /* sentinel */
2364+
};
2365+
/**end repeat**/
2366+
2367+
23312368
/************* As_mapping functions for void array scalar ************/
23322369

23332370
static Py_ssize_t
@@ -4104,6 +4141,17 @@ initialize_numeric_types(void)
41044141

41054142
/**end repeat**/
41064143

4144+
/**begin repeat
4145+
* #name = byte, short, int, long, longlong,
4146+
* ubyte, ushort, uint, ulong, ulonglong#
4147+
* #Name = Byte, Short, Int, Long, LongLong,
4148+
* UByte, UShort, UInt, ULong, ULongLong#
4149+
*/
4150+
4151+
Py@Name@ArrType_Type.tp_methods = @name@type_methods;
4152+
4153+
/**end repeat**/
4154+
41074155
/**begin repeat
41084156
* #name = half, float, double, longdouble#
41094157
* #Name = Half, Float, Double, LongDouble#

numpy/core/src/npymath/npy_math_internal.h.src

+86
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,29 @@
5555
*/
5656
#include "npy_math_private.h"
5757

58+
/* Magic binary numbers used by bit_count
59+
* For type T, the magic numbers are computed as follows:
60+
* Magic[0]: 01 01 01 01 01 01... = (T)~(T)0/3
61+
* Magic[1]: 0011 0011 0011... = (T)~(T)0/15 * 3
62+
* Magic[2]: 00001111 00001111... = (T)~(T)0/255 * 15
63+
* Magic[3]: 00000001 00000001... = (T)~(T)0/255
64+
*
65+
* Counting bits set, in parallel
66+
* Based on: http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel
67+
*
68+
* Generic Algorithm for type T:
69+
* a = a - ((a >> 1) & (T)~(T)0/3);
70+
* a = (a & (T)~(T)0/15*3) + ((a >> 2) & (T)~(T)0/15*3);
71+
* a = (a + (a >> 4)) & (T)~(T)0/255*15;
72+
* c = (T)(a * ((T)~(T)0/255)) >> (sizeof(T) - 1) * CHAR_BIT;
73+
*/
74+
75+
static const npy_uint8 MAGIC8[] = {0x55u, 0x33u, 0x0Fu, 0x01u};
76+
static const npy_uint16 MAGIC16[] = {0x5555u, 0x3333u, 0x0F0Fu, 0x0101u};
77+
static const npy_uint32 MAGIC32[] = {0x55555555ul, 0x33333333ul, 0x0F0F0F0Ful, 0x01010101ul};
78+
static const npy_uint64 MAGIC64[] = {0x5555555555555555ull, 0x3333333333333333ull, 0x0F0F0F0F0F0F0F0Full, 0x0101010101010101ull};
79+
80+
5881
/*
5982
*****************************************************************************
6083
** BASIC MATH FUNCTIONS **
@@ -814,3 +837,66 @@ npy_rshift@u@@c@(npy_@u@@type@ a, npy_@u@@type@ b)
814837
}
815838
/**end repeat1**/
816839
/**end repeat**/
840+
841+
842+
#define __popcnt32 __popcnt
843+
/**begin repeat
844+
*
845+
* #type = ubyte, ushort, uint, ulong, ulonglong#
846+
* #STYPE = BYTE, SHORT, INT, LONG, LONGLONG#
847+
* #c = hh, h, , l, ll#
848+
*/
849+
#undef TO_BITS_LEN
850+
#if 0
851+
/**begin repeat1
852+
* #len = 8, 16, 32, 64#
853+
*/
854+
#elif NPY_BITSOF_@STYPE@ == @len@
855+
#define TO_BITS_LEN(X) X##@len@
856+
/**end repeat1**/
857+
#endif
858+
859+
860+
NPY_INPLACE uint8_t
861+
npy_popcount_parallel@c@(npy_@type@ a)
862+
{
863+
a = a - ((a >> 1) & (npy_@type@) TO_BITS_LEN(MAGIC)[0]);
864+
a = ((a & (npy_@type@) TO_BITS_LEN(MAGIC)[1])) + ((a >> 2) & (npy_@type@) TO_BITS_LEN(MAGIC)[1]);
865+
a = (a + (a >> 4)) & (npy_@type@) TO_BITS_LEN(MAGIC)[2];
866+
return (npy_@type@) (a * (npy_@type@) TO_BITS_LEN(MAGIC)[3]) >> ((NPY_SIZEOF_@STYPE@ - 1) * CHAR_BIT);
867+
}
868+
869+
NPY_INPLACE uint8_t
870+
npy_popcountu@c@(npy_@type@ a)
871+
{
872+
/* use built-in popcount if present, else use our implementation */
873+
#if (defined(__clang__) || defined(__GNUC__)) && NPY_BITSOF_@STYPE@ >= 32
874+
return __builtin_popcount@c@(a);
875+
#elif defined(_MSC_VER) && NPY_BITSOF_@STYPE@ >= 16
876+
/* no builtin __popcnt64 for 32 bits */
877+
#if defined(_WIN64) || (defined(_WIN32) && NPY_BITSOF_@STYPE@ != 64)
878+
return TO_BITS_LEN(__popcnt)(a);
879+
/* split 64 bit number into two 32 bit ints and return sum of counts */
880+
#elif (defined(_WIN32) && NPY_BITSOF_@STYPE@ == 64)
881+
npy_uint32 left = (npy_uint32) (a>>32);
882+
npy_uint32 right = (npy_uint32) a;
883+
return __popcnt32(left) + __popcnt32(right);
884+
#endif
885+
#else
886+
return npy_popcount_parallel@c@(a);
887+
#endif
888+
}
889+
/**end repeat**/
890+
891+
/**begin repeat
892+
*
893+
* #type = byte, short, int, long, longlong#
894+
* #c = hh, h, , l, ll#
895+
*/
896+
NPY_INPLACE uint8_t
897+
npy_popcount@c@(npy_@type@ a)
898+
{
899+
/* Return popcount of abs(a) */
900+
return npy_popcountu@c@(a < 0 ? -a : a);
901+
}
902+
/**end repeat**/

numpy/core/tests/test_scalar_methods.py

+18
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,21 @@ def test_class_getitem_38(cls: Type[np.number]) -> None:
183183
match = "Type subscription requires python >= 3.9"
184184
with pytest.raises(TypeError, match=match):
185185
cls[Any]
186+
187+
188+
class TestBitCount:
189+
# derived in part from the cpython test "test_bit_count"
190+
191+
@pytest.mark.parametrize("itype", np.sctypes['int']+np.sctypes['uint'])
192+
def test_small(self, itype):
193+
for a in range(max(np.iinfo(itype).min, 0), 128):
194+
msg = f"Smoke test for {itype}({a}).bit_count()"
195+
assert itype(a).bit_count() == bin(a).count("1"), msg
196+
197+
def test_bit_count(self):
198+
for exp in [10, 17, 63]:
199+
a = 2**exp
200+
assert np.uint64(a).bit_count() == 1
201+
assert np.uint64(a - 1).bit_count() == exp
202+
assert np.uint64(a ^ 63).bit_count() == 7
203+
assert np.uint64((a - 1) ^ 510).bit_count() == exp - 8

0 commit comments

Comments
 (0)