|
8 | 8 | # include "numpy/arrayscalars.h" |
9 | 9 | # include "numpy/halffloat.h" |
10 | 10 | # include <string.h> |
| 11 | +# include <math.h> |
11 | 12 |
|
12 | 13 | # ifdef _WIN32 |
13 | 14 | # include <io.h> |
@@ -1128,6 +1129,202 @@ group_ordering(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs) |
1128 | 1129 | return NULL; |
1129 | 1130 | } |
1130 | 1131 |
|
| 1132 | +typedef enum { |
| 1133 | + GR_SUM, |
| 1134 | + GR_PROD, |
| 1135 | + GR_MIN, |
| 1136 | + GR_MAX, |
| 1137 | + GR_COUNT, |
| 1138 | +} AK_GroupReduceOp; |
| 1139 | + |
| 1140 | +static int |
| 1141 | +AK_group_reduce_op_from_str(const char *op, AK_GroupReduceOp *out) { |
| 1142 | + if (strcmp(op, "sum") == 0) { *out = GR_SUM; return 0; } |
| 1143 | + if (strcmp(op, "prod") == 0) { *out = GR_PROD; return 0; } |
| 1144 | + if (strcmp(op, "min") == 0) { *out = GR_MIN; return 0; } |
| 1145 | + if (strcmp(op, "max") == 0) { *out = GR_MAX; return 0; } |
| 1146 | + if (strcmp(op, "count") == 0) { *out = GR_COUNT; return 0; } |
| 1147 | + PyErr_Format(PyExc_ValueError, |
| 1148 | + "unknown op '%s'; expected one of sum, prod, min, max, count", op); |
| 1149 | + return -1; |
| 1150 | +} |
| 1151 | + |
| 1152 | +// Accumulate `n` float64 into `out[size]` per group. NaN propagates for min/max |
| 1153 | +// (matching np.min/np.max, not the nan-skipping variants). |
| 1154 | +static void |
| 1155 | +AK_group_reduce_f64( |
| 1156 | + const npy_float64 *v, |
| 1157 | + const npy_intp *codes, |
| 1158 | + npy_intp n, |
| 1159 | + npy_float64 *out, |
| 1160 | + npy_intp size, |
| 1161 | + AK_GroupReduceOp op) { |
| 1162 | + npy_float64 init; |
| 1163 | + switch (op) { |
| 1164 | + case GR_PROD: init = 1.0; break; |
| 1165 | + case GR_MIN: init = NPY_INFINITY; break; |
| 1166 | + case GR_MAX: init = -NPY_INFINITY; break; |
| 1167 | + default: init = 0.0; break; // GR_SUM |
| 1168 | + } |
| 1169 | + for (npy_intp g = 0; g < size; g++) { |
| 1170 | + out[g] = init; |
| 1171 | + } |
| 1172 | + for (npy_intp i = 0; i < n; i++) { |
| 1173 | + npy_intp g = codes[i]; |
| 1174 | + npy_float64 x = v[i]; |
| 1175 | + switch (op) { |
| 1176 | + case GR_SUM: out[g] += x; break; |
| 1177 | + case GR_PROD: out[g] *= x; break; |
| 1178 | + case GR_MIN: if (isnan(x) || x < out[g]) out[g] = x; break; |
| 1179 | + case GR_MAX: if (isnan(x) || x > out[g]) out[g] = x; break; |
| 1180 | + default: break; |
| 1181 | + } |
| 1182 | + } |
| 1183 | +} |
| 1184 | + |
| 1185 | +// Accumulate `n` int64 into `out[size]` per group. |
| 1186 | +static void |
| 1187 | +AK_group_reduce_i64( |
| 1188 | + const npy_int64 *v, |
| 1189 | + const npy_intp *codes, |
| 1190 | + npy_intp n, |
| 1191 | + npy_int64 *out, |
| 1192 | + npy_intp size, |
| 1193 | + AK_GroupReduceOp op) { |
| 1194 | + npy_int64 init; |
| 1195 | + switch (op) { |
| 1196 | + case GR_PROD: init = 1; break; |
| 1197 | + case GR_MIN: init = NPY_MAX_INT64; break; |
| 1198 | + case GR_MAX: init = NPY_MIN_INT64; break; |
| 1199 | + default: init = 0; break; // GR_SUM |
| 1200 | + } |
| 1201 | + for (npy_intp g = 0; g < size; g++) { |
| 1202 | + out[g] = init; |
| 1203 | + } |
| 1204 | + for (npy_intp i = 0; i < n; i++) { |
| 1205 | + npy_intp g = codes[i]; |
| 1206 | + npy_int64 x = v[i]; |
| 1207 | + switch (op) { |
| 1208 | + case GR_SUM: out[g] += x; break; |
| 1209 | + case GR_PROD: out[g] *= x; break; |
| 1210 | + case GR_MIN: if (x < out[g]) out[g] = x; break; |
| 1211 | + case GR_MAX: if (x > out[g]) out[g] = x; break; |
| 1212 | + default: break; |
| 1213 | + } |
| 1214 | + } |
| 1215 | +} |
| 1216 | + |
| 1217 | +static char *group_reduce_kwarg_names[] = { |
| 1218 | + "codes", |
| 1219 | + "size", |
| 1220 | + "values", |
| 1221 | + "op", |
| 1222 | + NULL |
| 1223 | +}; |
| 1224 | + |
| 1225 | +// Grouped reduction. Given dense group `codes` in [0, size), a 1D |
| 1226 | +// `values` array, and an `op` ('sum'/'prod'/'min'/'max'/'count'), return a length- |
| 1227 | +// `size` array of per-group results in code order. Accumulates directly by code in |
| 1228 | +// an O(n) pass after validating codes (no sort, no reorder). 'count' returns int64 group sizes and ignores |
| 1229 | +// the values dtype; other ops return the values dtype (float64 or int64). This is |
| 1230 | +// the vectorized replacement for a per-group Python reduction loop. |
| 1231 | +PyObject * |
| 1232 | +group_reduce(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs) |
| 1233 | +{ |
| 1234 | + PyArrayObject *codes = NULL; |
| 1235 | + Py_ssize_t size = 0; |
| 1236 | + PyArrayObject *values = NULL; |
| 1237 | + const char *op_name = NULL; |
| 1238 | + |
| 1239 | + if (!PyArg_ParseTupleAndKeywords(args, kwargs, |
| 1240 | + "O!nO!s:group_reduce", |
| 1241 | + group_reduce_kwarg_names, |
| 1242 | + &PyArray_Type, &codes, |
| 1243 | + &size, |
| 1244 | + &PyArray_Type, &values, |
| 1245 | + &op_name |
| 1246 | + )) { |
| 1247 | + return NULL; |
| 1248 | + } |
| 1249 | + AK_GroupReduceOp op; |
| 1250 | + if (AK_group_reduce_op_from_str(op_name, &op)) { |
| 1251 | + return NULL; |
| 1252 | + } |
| 1253 | + if (size < 0) { |
| 1254 | + PyErr_SetString(PyExc_ValueError, "size must be non-negative"); |
| 1255 | + return NULL; |
| 1256 | + } |
| 1257 | + if (PyArray_NDIM(codes) != 1 || PyArray_NDIM(values) != 1) { |
| 1258 | + PyErr_SetString(PyExc_ValueError, "Arrays must be 1-dimensional"); |
| 1259 | + return NULL; |
| 1260 | + } |
| 1261 | + if (PyArray_TYPE(codes) != NPY_INTP) { |
| 1262 | + PyErr_SetString(PyExc_ValueError, "codes must be of type intp"); |
| 1263 | + return NULL; |
| 1264 | + } |
| 1265 | + if (!PyArray_IS_C_CONTIGUOUS(codes) || !PyArray_IS_C_CONTIGUOUS(values)) { |
| 1266 | + PyErr_SetString(PyExc_ValueError, "Arrays must be contiguous"); |
| 1267 | + return NULL; |
| 1268 | + } |
| 1269 | + npy_intp n = PyArray_SIZE(codes); |
| 1270 | + if (PyArray_SIZE(values) != n) { |
| 1271 | + PyErr_SetString(PyExc_ValueError, |
| 1272 | + "codes and values must be the same length"); |
| 1273 | + return NULL; |
| 1274 | + } |
| 1275 | + const npy_intp *codes_buffer = (npy_intp*)PyArray_DATA(codes); |
| 1276 | + // validate codes are in range before any indexed writes into the output |
| 1277 | + for (npy_intp i = 0; i < n; i++) { |
| 1278 | + npy_intp c = codes_buffer[i]; |
| 1279 | + if (c < 0 || c >= size) { |
| 1280 | + PyErr_Format(PyExc_ValueError, |
| 1281 | + "code %zd out of range [0, %zd)", |
| 1282 | + (Py_ssize_t)c, (Py_ssize_t)size); |
| 1283 | + return NULL; |
| 1284 | + } |
| 1285 | + } |
| 1286 | + |
| 1287 | + npy_intp dims[1] = {size}; |
| 1288 | + int vtype = PyArray_TYPE(values); |
| 1289 | + |
| 1290 | + if (op == GR_COUNT) { |
| 1291 | + PyObject *out_arr = PyArray_ZEROS(1, dims, NPY_INT64, 0); |
| 1292 | + if (!out_arr) { |
| 1293 | + return NULL; |
| 1294 | + } |
| 1295 | + npy_int64 *out = (npy_int64*)PyArray_DATA((PyArrayObject*)out_arr); |
| 1296 | + for (npy_intp i = 0; i < n; i++) { |
| 1297 | + out[codes_buffer[i]]++; |
| 1298 | + } |
| 1299 | + PyArray_CLEARFLAGS((PyArrayObject*)out_arr, NPY_ARRAY_WRITEABLE); |
| 1300 | + return out_arr; |
| 1301 | + } |
| 1302 | + |
| 1303 | + if (vtype != NPY_DOUBLE && vtype != NPY_INT64) { |
| 1304 | + PyErr_SetString(PyExc_ValueError, |
| 1305 | + "values must be of type float64 or int64"); |
| 1306 | + return NULL; |
| 1307 | + } |
| 1308 | + PyObject *out_arr = PyArray_EMPTY(1, dims, vtype, 0); |
| 1309 | + if (!out_arr) { |
| 1310 | + return NULL; |
| 1311 | + } |
| 1312 | + if (vtype == NPY_DOUBLE) { |
| 1313 | + AK_group_reduce_f64( |
| 1314 | + (npy_float64*)PyArray_DATA(values), |
| 1315 | + codes_buffer, n, |
| 1316 | + (npy_float64*)PyArray_DATA((PyArrayObject*)out_arr), size, op); |
| 1317 | + } |
| 1318 | + else { |
| 1319 | + AK_group_reduce_i64( |
| 1320 | + (npy_int64*)PyArray_DATA(values), |
| 1321 | + codes_buffer, n, |
| 1322 | + (npy_int64*)PyArray_DATA((PyArrayObject*)out_arr), size, op); |
| 1323 | + } |
| 1324 | + PyArray_CLEARFLAGS((PyArrayObject*)out_arr, NPY_ARRAY_WRITEABLE); |
| 1325 | + return out_arr; |
| 1326 | +} |
| 1327 | + |
1131 | 1328 | // Fill one strided lane in place: walk positions in the fill direction, carrying |
1132 | 1329 | // the most recent non-target value into each target position (subject to `limit` |
1133 | 1330 | // consecutive fills per run). `elem_base`/`elem_stride` address elements in bytes; |
|
0 commit comments