Skip to content

Commit 11eefea

Browse files
authored
Bincount (#738)
* working skeletoon of bincount * integer arrays can be binned * add keyword handling * add tests, fix small glitches in code * add documentation * correct changelog
1 parent 068da5f commit 11eefea

File tree

9 files changed

+335
-10
lines changed

9 files changed

+335
-10
lines changed

code/numpy/compare.c

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*
77
* The MIT License (MIT)
88
*
9-
* Copyright (c) 2020-2021 Zoltán Vörös
9+
* Copyright (c) 2020-2025 Zoltán Vörös
1010
* 2020 Jeff Epler for Adafruit Industries
1111
*/
1212

@@ -23,6 +23,136 @@
2323
#include "carray/carray_tools.h"
2424
#include "compare.h"
2525

26+
#ifdef ULAB_NUMPY_HAS_BINCOUNT
27+
mp_obj_t compare_bincount(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
28+
static const mp_arg_t allowed_args[] = {
29+
{ MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_rom_obj = MP_ROM_NONE} } ,
30+
{ MP_QSTR_weights, MP_ARG_OBJ | MP_ARG_KW_ONLY, { .u_rom_obj = MP_ROM_NONE } },
31+
{ MP_QSTR_minlength, MP_ARG_OBJ | MP_ARG_KW_ONLY, { .u_rom_obj = MP_ROM_NONE } },
32+
};
33+
34+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
35+
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
36+
37+
if(!mp_obj_is_type(args[0].u_obj, &ulab_ndarray_type)) {
38+
mp_raise_TypeError(MP_ERROR_TEXT("input must be an ndarray"));
39+
}
40+
ndarray_obj_t *input = MP_OBJ_TO_PTR(args[0].u_obj);
41+
42+
#if ULAB_MAX_DIMS > 1
43+
// no need to check anything, if the maximum number of dimensions is 1
44+
if(input->ndim != 1) {
45+
mp_raise_ValueError(MP_ERROR_TEXT("object too deep for desired array"));
46+
}
47+
#endif
48+
if((input->dtype != NDARRAY_UINT8) && (input->dtype != NDARRAY_UINT16)) {
49+
mp_raise_TypeError(MP_ERROR_TEXT("cannot cast array data from dtype"));
50+
}
51+
52+
// first find the maximum of the array, and figure out how long the result should be
53+
size_t length = 0;
54+
int32_t stride = input->strides[ULAB_MAX_DIMS - 1];
55+
if(input->dtype == NDARRAY_UINT8) {
56+
uint8_t *iarray = (uint8_t *)input->array;
57+
for(size_t i = 0; i < input->len; i++) {
58+
if(*iarray > length) {
59+
length = *iarray;
60+
}
61+
iarray += stride;
62+
}
63+
} else if(input->dtype == NDARRAY_UINT16) {
64+
stride /= 2;
65+
uint16_t *iarray = (uint16_t *)input->array;
66+
for(size_t i = 0; i < input->len; i++) {
67+
if(*iarray > length) {
68+
length = *iarray;
69+
}
70+
iarray += stride;
71+
}
72+
}
73+
length += 1;
74+
75+
if(args[2].u_obj != mp_const_none) {
76+
int32_t minlength = mp_obj_get_int(args[2].u_obj);
77+
if(minlength < 0) {
78+
mp_raise_ValueError(MP_ERROR_TEXT("minlength must not be negative"));
79+
}
80+
if((size_t)minlength > length) {
81+
length = minlength;
82+
}
83+
} else {
84+
if(input->len == 0) {
85+
length = 0;
86+
}
87+
}
88+
89+
ndarray_obj_t *result = NULL;
90+
ndarray_obj_t *weights = NULL;
91+
92+
if(args[1].u_obj == mp_const_none) {
93+
result = ndarray_new_linear_array(length, NDARRAY_UINT16);
94+
} else {
95+
if(!mp_obj_is_type(args[1].u_obj, &ulab_ndarray_type)) {
96+
mp_raise_TypeError(MP_ERROR_TEXT("input must be an ndarray"));
97+
}
98+
weights = MP_OBJ_TO_PTR(args[1].u_obj);
99+
if(weights->len < input->len) {
100+
mp_raise_ValueError(MP_ERROR_TEXT("the weights and list don't have the same length"));
101+
}
102+
#if ULAB_SUPPORTS_COMPLEX
103+
if(weights->dtype == NDARRAY_COMPLEX) {
104+
mp_raise_TypeError(MP_ERROR_TEXT("cannot cast weigths to float"));
105+
}
106+
#endif /* ULAB_SUPPORTS_COMPLEX */
107+
108+
result = ndarray_new_linear_array(length, NDARRAY_FLOAT);
109+
}
110+
111+
// now we can do the binning
112+
if(result->dtype == NDARRAY_UINT16) {
113+
uint16_t *rarray = (uint16_t *)result->array;
114+
if(input->dtype == NDARRAY_UINT8) {
115+
uint8_t *iarray = (uint8_t *)input->array;
116+
for(size_t i = 0; i < input->len; i++) {
117+
rarray[*iarray] += 1;
118+
iarray += stride;
119+
}
120+
} else if(input->dtype == NDARRAY_UINT16) {
121+
uint16_t *iarray = (uint16_t *)input->array;
122+
for(size_t i = 0; i < input->len; i++) {
123+
rarray[*iarray] += 1;
124+
iarray += stride;
125+
}
126+
}
127+
} else {
128+
mp_float_t *rarray = (mp_float_t *)result->array;
129+
130+
mp_float_t (*get_weights)(void *) = ndarray_get_float_function(weights->dtype);
131+
uint8_t *warray = (uint8_t *)weights->array;
132+
133+
if(input->dtype == NDARRAY_UINT8) {
134+
uint8_t *iarray = (uint8_t *)input->array;
135+
for(size_t i = 0; i < input->len; i++) {
136+
rarray[*iarray] += get_weights(warray);
137+
iarray += stride;
138+
warray += weights->strides[ULAB_MAX_DIMS - 1];
139+
}
140+
} else if(input->dtype == NDARRAY_UINT16) {
141+
uint16_t *iarray = (uint16_t *)input->array;
142+
for(size_t i = 0; i < input->len; i++) {
143+
rarray[*iarray] += get_weights(warray);
144+
iarray += stride;
145+
warray += weights->strides[ULAB_MAX_DIMS - 1];
146+
}
147+
}
148+
}
149+
150+
return MP_OBJ_FROM_PTR(result);
151+
}
152+
153+
MP_DEFINE_CONST_FUN_OBJ_KW(compare_bincount_obj, 1, compare_bincount);
154+
#endif /* ULAB_NUMPY_HAS_BINCOUNT */
155+
26156
static mp_obj_t compare_function(mp_obj_t x1, mp_obj_t x2, uint8_t op) {
27157
ndarray_obj_t *lhs = ndarray_from_mp_obj(x1, 0);
28158
ndarray_obj_t *rhs = ndarray_from_mp_obj(x2, 0);

code/numpy/compare.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*
77
* The MIT License (MIT)
88
*
9-
* Copyright (c) 2020-2021 Zoltán Vörös
9+
* Copyright (c) 2020-2025 Zoltán Vörös
1010
*/
1111

1212
#ifndef _COMPARE_
@@ -23,6 +23,7 @@ enum COMPARE_FUNCTION_TYPE {
2323
COMPARE_CLIP,
2424
};
2525

26+
MP_DECLARE_CONST_FUN_OBJ_KW(compare_bincount_obj);
2627
MP_DECLARE_CONST_FUN_OBJ_3(compare_clip_obj);
2728
MP_DECLARE_CONST_FUN_OBJ_2(compare_equal_obj);
2829
MP_DECLARE_CONST_FUN_OBJ_2(compare_isfinite_obj);

code/numpy/numpy.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ static const mp_rom_map_elem_t ulab_numpy_globals_table[] = {
169169
#if ULAB_NUMPY_HAS_ZEROS
170170
{ MP_ROM_QSTR(MP_QSTR_zeros), MP_ROM_PTR(&create_zeros_obj) },
171171
#endif
172+
#if ULAB_NUMPY_HAS_BINCOUNT
173+
{ MP_ROM_QSTR(MP_QSTR_bincount), MP_ROM_PTR(&compare_bincount_obj) },
174+
#endif
172175
#if ULAB_NUMPY_HAS_CLIP
173176
{ MP_ROM_QSTR(MP_QSTR_clip), MP_ROM_PTR(&compare_clip_obj) },
174177
#endif

code/ulab.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
#include "user/user.h"
3434
#include "utils/utils.h"
3535

36-
#define ULAB_VERSION 6.9.0
36+
#define ULAB_VERSION 6.10.0
3737
#define xstr(s) str(s)
3838
#define str(s) #s
3939

code/ulab.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,10 @@
374374
#endif
375375

376376
// functions that compare arrays
377+
#ifndef ULAB_NUMPY_HAS_BINCOUNT
378+
#define ULAB_NUMPY_HAS_BINCOUNT (1)
379+
#endif
380+
377381
#ifndef ULAB_NUMPY_HAS_CLIP
378382
#define ULAB_NUMPY_HAS_CLIP (1)
379383
#endif
@@ -413,7 +417,7 @@
413417
// the integrate module; functions of the integrate module still have
414418
// to be defined separately
415419
#ifndef ULAB_SCIPY_HAS_INTEGRATE_MODULE
416-
#define ULAB_SCIPY_HAS_INTEGRATE_MODULE (1)
420+
#define ULAB_SCIPY_HAS_INTEGRATE_MODULE (1)
417421
#endif
418422

419423
#ifndef ULAB_INTEGRATE_HAS_TANHSINH

docs/numpy-functions.ipynb

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@
238238
"1. [numpy.argmin](#argmin)\n",
239239
"1. [numpy.argsort](#argsort)\n",
240240
"1. [numpy.asarray*](#asarray)\n",
241+
"1. [numpy.bincount](#bincount)\n",
241242
"1. [numpy.bitwise_and](#bitwise_and)\n",
242243
"1. [numpy.bitwise_or](#bitwise_and)\n",
243244
"1. [numpy.bitwise_xor](#bitwise_and)\n",
@@ -612,6 +613,68 @@
612613
"print('a == c: {}'.format(a is c))"
613614
]
614615
},
616+
{
617+
"cell_type": "markdown",
618+
"metadata": {},
619+
"source": [
620+
"## bincount\n",
621+
"\n",
622+
"`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.bincount.htm\n",
623+
"\n",
624+
"This method counts number of occurrences of each value in array of non-negative integers. A method accepts a single positional, and two keyword arguments, `weights`, and `minlength`. The positional arguments is assumed to be a one-dimensional array of unsigned integers that are either 8 or 16 bits wide. For other types, a `TypeError` is raised.\n",
625+
"\n",
626+
"The `weights` keyword argument can be supplied to apply weigths to the counts. In this case, the return value is a `float` array, otherwise, it is a `uint16`. The `weights` are assumed to be a one-dimensional `ndarray`, otherwise, a `TypeError` is raised. \n",
627+
"\n",
628+
"`minlength` can be supplied to give a lower bound to the length of the `ndarray` returned. In this case, the unused slots are filled with zeros."
629+
]
630+
},
631+
{
632+
"cell_type": "code",
633+
"execution_count": 15,
634+
"metadata": {},
635+
"outputs": [
636+
{
637+
"name": "stdout",
638+
"output_type": "stream",
639+
"text": [
640+
"a: array([0, 1, 2, 3, 4, 5, 6, 7], dtype=uint8)\n",
641+
"bincount(a): array([1, 1, 1, 1, 1, 1, 1, 1], dtype=uint16)\n",
642+
"\n",
643+
"a: array([0, 0, 1, 1, 1, 2, 2, 2, 2], dtype=uint8)\n",
644+
"bincount(a): array([2, 3, 4], dtype=uint16)\n",
645+
"\n",
646+
"a: array([0, 1, 2, 3, 4, 5, 6, 7], dtype=uint8)\n",
647+
"w: array([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8], dtype=float64)\n",
648+
"bincount(a, weights=w): array([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8], dtype=float64)\n",
649+
"\n",
650+
"a: array([0, 1, 2, 3], dtype=uint8)\n",
651+
"bincount(a, minlength=8): array([1, 1, 1, 1, 0, 0, 0, 0], dtype=uint16)\n",
652+
"\n",
653+
"\n"
654+
]
655+
}
656+
],
657+
"source": [
658+
"%%micropython -unix 1\n",
659+
"\n",
660+
"from ulab import numpy as np\n",
661+
"\n",
662+
"a = np.array(range(8), dtype=np.uint8)\n",
663+
"\n",
664+
"print(f'a: {a}\\nbincount(a): {np.bincount(a)}')\n",
665+
"\n",
666+
"a = np.array([0, 0, 1, 1, 1, 2, 2, 2, 2], dtype=np.uint8)\n",
667+
"print(f'\\na: {a}\\nbincount(a): {np.bincount(a)}')\n",
668+
"\n",
669+
"a = np.array(range(8), dtype=np.uint8)\n",
670+
"w = np.array([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8])\n",
671+
"\n",
672+
"print(f'\\na: {a}\\nw: {w}\\nbincount(a, weights=w): {np.bincount(a, weights=w)}')\n",
673+
"\n",
674+
"a = np.array(range(4), dtype=np.uint8)\n",
675+
"print(f'\\na: {a}\\nbincount(a, minlength=8): {np.bincount(a, minlength=8)}')\n"
676+
]
677+
},
615678
{
616679
"cell_type": "markdown",
617680
"metadata": {},
@@ -2944,7 +3007,7 @@
29443007
],
29453008
"metadata": {
29463009
"kernelspec": {
2947-
"display_name": "Python 3.8.5 ('base')",
3010+
"display_name": "base",
29483011
"language": "python",
29493012
"name": "python3"
29503013
},
@@ -3006,11 +3069,6 @@
30063069
"_Feature"
30073070
],
30083071
"window_display": false
3009-
},
3010-
"vscode": {
3011-
"interpreter": {
3012-
"hash": "9e4ec6f642f986afcc9e252c165e44859a62defc5c697cae6f82c2943465ec10"
3013-
}
30143072
}
30153073
},
30163074
"nbformat": 4,

docs/ulab-change-log.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
Wed, 3 Sep 2025
2+
3+
version 6.10.0
4+
5+
add bincount
6+
17
Fri, 06 Jun 2025
28

39
version 6.8.0

tests/2d/numpy/bincount.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
try:
2+
from ulab import numpy as np
3+
except:
4+
import numpy as np
5+
6+
for dtype in (np.uint8, np.uint16):
7+
a = np.array([0, 1, 1, 3, 3, 3], dtype=dtype)
8+
print(np.bincount(a))
9+
10+
for dtype in (np.uint8, np.uint16):
11+
a = np.array([0, 2, 2, 4], dtype=dtype)
12+
print(np.bincount(a, minlength=3))
13+
14+
for dtype in (np.uint8, np.uint16):
15+
a = np.array([0, 2, 2, 4], dtype=dtype)
16+
print(np.bincount(a, minlength=8))
17+
18+
for dtype in (np.uint8, np.uint16):
19+
a = np.array([], dtype=dtype)
20+
print(np.bincount(a))
21+
print(np.bincount(a, minlength=8))
22+
23+
for dtype in (np.uint8, np.uint16):
24+
a = np.array([0, 1, 1, 3], dtype=dtype)
25+
w = np.array([0.5, 1.0, 2.5, 0.25])
26+
print(np.where(abs(np.bincount(a, weights=w) - np.array([0.5, 2.0, 0.0, 0.25])) < 0.001, 1, 0))
27+
28+
w = np.array([1, 2, 3, 4], dtype=np.uint8)
29+
print(np.bincount(a, weights=w))
30+
31+
for dtype in (np.uint8, np.uint16):
32+
a = np.array([1, 1], dtype=dtype)
33+
w = np.array([0.5, 1.5])
34+
print(np.bincount(a, weights=w, minlength=4))
35+
36+
for dtype in (np.uint8, np.uint16):
37+
a = np.array([2, 2, 2, 3], dtype=dtype)
38+
for wtype in (np.uint8, np.uint16, np.int8, np.int16, np.float):
39+
w = np.array([1, 2, 3, 4], dtype=wtype)
40+
print(np.bincount(a, weights=w))
41+
42+
for dtype in (np.int8, np.int16, np.float):
43+
a = np.array([2, 2, 2, 3], dtype=dtype)
44+
try:
45+
np.bincount(a)
46+
except Exception as e:
47+
print(e)
48+
49+
for dtype in (np.uint8, np.int8, np.uint16, np.int16, np.float):
50+
a = np.array(range(4), dtype=dtype).reshape((2, 2))
51+
try:
52+
np.bincount(a)
53+
except Exception as e:
54+
print(e)
55+
56+
for dtype in (np.uint8, np.uint16):
57+
a = np.array([1, 2, 3], dtype=dtype)
58+
w = np.array([1, 2])
59+
try:
60+
np.bincount(a, weights=w)
61+
except Exception as e:
62+
print(e)
63+
64+
for dtype in (np.uint8, np.uint16):
65+
a = np.array([1, 2, 3], dtype=dtype)
66+
try:
67+
np.bincount(a, minlength=-1)
68+
except Exception as e:
69+
print(e)
70+
71+
for dtype in (np.uint8, np.uint16):
72+
a = np.array([1, 2, 3], dtype=dtype)
73+
w = np.array([1j, 2j, 3j], dtype=np.complex)
74+
try:
75+
np.bincount(a, weights=w)
76+
except Exception as e:
77+
print(e)
78+
79+
80+
a = np.array([0, 1000], dtype=np.uint16)
81+
y = np.bincount(a)
82+
print(y[0], y[1000], len(y))

0 commit comments

Comments
 (0)