arraykit 1.9.0__tar.gz → 1.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arraykit-1.9.0/arraykit.egg-info → arraykit-1.11.0}/PKG-INFO +13 -1
- {arraykit-1.9.0 → arraykit-1.11.0}/README.rst +12 -0
- arraykit-1.11.0/VERSION +2 -0
- {arraykit-1.9.0 → arraykit-1.11.0/arraykit.egg-info}/PKG-INFO +13 -1
- {arraykit-1.9.0 → arraykit-1.11.0}/arraykit.egg-info/SOURCES.txt +1 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/__init__.py +1 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/__init__.pyi +4 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/_arraykit.c +4 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/methods.c +275 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/methods.h +3 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/tri_map.c +55 -0
- arraykit-1.11.0/test/test_group_reduce.py +217 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_tri_map.py +154 -0
- arraykit-1.9.0/VERSION +0 -2
- {arraykit-1.9.0 → arraykit-1.11.0}/LICENSE.txt +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/MANIFEST.in +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/arraykit.egg-info/dependency_links.txt +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/arraykit.egg-info/requires.txt +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/arraykit.egg-info/top_level.txt +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/pyproject.toml +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/setup.cfg +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/setup.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/array_go.c +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/array_go.h +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/array_to_tuple.c +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/array_to_tuple.h +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/auto_map.c +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/auto_map.h +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/block_index.c +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/block_index.h +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/delimited_to_arrays.c +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/delimited_to_arrays.h +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/py.typed +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/tri_map.h +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/src/utilities.h +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_array_go.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_astype_array.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_auto_map.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_auto_map_property.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_block_index.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_delimited_to_arrays.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_delimited_to_arrays_integration.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_delimited_to_arrays_property.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_factorize.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_fill_directional.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_group_ordering.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_nonzero_1d.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_nonzero_1d_property.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_objectable.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_pyi.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_split_after_count.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_transition_slices_from_group.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_type_discovery.py +0 -0
- {arraykit-1.9.0 → arraykit-1.11.0}/test/test_util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arraykit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.11.0
|
|
4
4
|
Summary: Array utilities for StaticFrame
|
|
5
5
|
Author: Christopher Ariza, Brandt Bucher, Charles Burkland
|
|
6
6
|
License: MIT
|
|
@@ -64,6 +64,18 @@ ArrayKit requires the following:
|
|
|
64
64
|
What is New in ArrayKit
|
|
65
65
|
-------------------------
|
|
66
66
|
|
|
67
|
+
1.11.0
|
|
68
|
+
............
|
|
69
|
+
|
|
70
|
+
Added ``group_reduce()``.
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
1.10.0
|
|
74
|
+
............
|
|
75
|
+
|
|
76
|
+
Added ``TriMap.register_pairs()``.
|
|
77
|
+
|
|
78
|
+
|
|
67
79
|
1.9.0
|
|
68
80
|
............
|
|
69
81
|
|
|
@@ -35,6 +35,18 @@ ArrayKit requires the following:
|
|
|
35
35
|
What is New in ArrayKit
|
|
36
36
|
-------------------------
|
|
37
37
|
|
|
38
|
+
1.11.0
|
|
39
|
+
............
|
|
40
|
+
|
|
41
|
+
Added ``group_reduce()``.
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
1.10.0
|
|
45
|
+
............
|
|
46
|
+
|
|
47
|
+
Added ``TriMap.register_pairs()``.
|
|
48
|
+
|
|
49
|
+
|
|
38
50
|
1.9.0
|
|
39
51
|
............
|
|
40
52
|
|
arraykit-1.11.0/VERSION
ADDED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arraykit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.11.0
|
|
4
4
|
Summary: Array utilities for StaticFrame
|
|
5
5
|
Author: Christopher Ariza, Brandt Bucher, Charles Burkland
|
|
6
6
|
License: MIT
|
|
@@ -64,6 +64,18 @@ ArrayKit requires the following:
|
|
|
64
64
|
What is New in ArrayKit
|
|
65
65
|
-------------------------
|
|
66
66
|
|
|
67
|
+
1.11.0
|
|
68
|
+
............
|
|
69
|
+
|
|
70
|
+
Added ``group_reduce()``.
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
1.10.0
|
|
74
|
+
............
|
|
75
|
+
|
|
76
|
+
Added ``TriMap.register_pairs()``.
|
|
77
|
+
|
|
78
|
+
|
|
67
79
|
1.9.0
|
|
68
80
|
............
|
|
69
81
|
|
|
@@ -27,6 +27,7 @@ from ._arraykit import get_new_indexers_and_screen as get_new_indexers_and_scree
|
|
|
27
27
|
from ._arraykit import write_array_to_file as write_array_to_file
|
|
28
28
|
from ._arraykit import factorize as factorize
|
|
29
29
|
from ._arraykit import group_ordering as group_ordering
|
|
30
|
+
from ._arraykit import group_reduce as group_reduce
|
|
30
31
|
from ._arraykit import fill_directional as fill_directional
|
|
31
32
|
from ._arraykit import count_iteration as count_iteration
|
|
32
33
|
from ._arraykit import first_true_1d as first_true_1d
|
|
@@ -54,6 +54,7 @@ class TriMap:
|
|
|
54
54
|
def __repr__(self) -> str: ...
|
|
55
55
|
def register_one(self, /, src_from: int, dst_from: int) -> None: ...
|
|
56
56
|
def register_many_from_one(self, __dst_pos: np.ndarray) -> None: ...
|
|
57
|
+
def register_pairs(self, __src_pos: np.ndarray, __dst_pos: np.ndarray) -> None: ...
|
|
57
58
|
def register_unmatched_dst(self) -> None: ...
|
|
58
59
|
def register_many(self, /, src_from: int, dst_from: np.ndarray) -> None: ...
|
|
59
60
|
def finalize(self) -> None: ...
|
|
@@ -232,6 +233,9 @@ def factorize(
|
|
|
232
233
|
def group_ordering(
|
|
233
234
|
codes: np.ndarray, *, size: tp.Optional[int] = ...
|
|
234
235
|
) -> tp.Tuple[np.ndarray, np.ndarray]: ...
|
|
236
|
+
def group_reduce(
|
|
237
|
+
codes: np.ndarray, size: int, values: np.ndarray, op: str
|
|
238
|
+
) -> np.ndarray: ...
|
|
235
239
|
def fill_directional(
|
|
236
240
|
array: np.ndarray,
|
|
237
241
|
target: np.ndarray,
|
|
@@ -78,6 +78,10 @@ static PyMethodDef arraykit_methods[] = {
|
|
|
78
78
|
(PyCFunction)group_ordering,
|
|
79
79
|
METH_VARARGS | METH_KEYWORDS,
|
|
80
80
|
NULL},
|
|
81
|
+
{"group_reduce",
|
|
82
|
+
(PyCFunction)group_reduce,
|
|
83
|
+
METH_VARARGS | METH_KEYWORDS,
|
|
84
|
+
NULL},
|
|
81
85
|
{"fill_directional",
|
|
82
86
|
(PyCFunction)fill_directional,
|
|
83
87
|
METH_VARARGS | METH_KEYWORDS,
|
|
@@ -8,6 +8,7 @@
|
|
|
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,280 @@ fail:
|
|
|
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
|
+
// Accumulate `n` uint64 into `out[size]` per group.
|
|
1218
|
+
static void
|
|
1219
|
+
AK_group_reduce_u64(
|
|
1220
|
+
const npy_uint64 *v,
|
|
1221
|
+
const npy_intp *codes,
|
|
1222
|
+
npy_intp n,
|
|
1223
|
+
npy_uint64 *out,
|
|
1224
|
+
npy_intp size,
|
|
1225
|
+
AK_GroupReduceOp op) {
|
|
1226
|
+
npy_uint64 init;
|
|
1227
|
+
switch (op) {
|
|
1228
|
+
case GR_PROD: init = 1; break;
|
|
1229
|
+
case GR_MIN: init = NPY_MAX_UINT64; break;
|
|
1230
|
+
case GR_MAX: init = 0; break;
|
|
1231
|
+
default: init = 0; break; // GR_SUM
|
|
1232
|
+
}
|
|
1233
|
+
for (npy_intp g = 0; g < size; g++) {
|
|
1234
|
+
out[g] = init;
|
|
1235
|
+
}
|
|
1236
|
+
for (npy_intp i = 0; i < n; i++) {
|
|
1237
|
+
npy_intp g = codes[i];
|
|
1238
|
+
npy_uint64 x = v[i];
|
|
1239
|
+
switch (op) {
|
|
1240
|
+
case GR_SUM: out[g] += x; break;
|
|
1241
|
+
case GR_PROD: out[g] *= x; break;
|
|
1242
|
+
case GR_MIN: if (x < out[g]) out[g] = x; break;
|
|
1243
|
+
case GR_MAX: if (x > out[g]) out[g] = x; break;
|
|
1244
|
+
default: break;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
static char *group_reduce_kwarg_names[] = {
|
|
1250
|
+
"codes",
|
|
1251
|
+
"size",
|
|
1252
|
+
"values",
|
|
1253
|
+
"op",
|
|
1254
|
+
NULL
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
// Grouped reduction. Given dense group `codes` in [0, size), a 1D `values` array, and
|
|
1258
|
+
// an `op` ('sum'/'prod'/'min'/'max'/'count'), return a length-`size` array of per-group
|
|
1259
|
+
// results in code order. Accumulates directly by code in an O(n) pass after validating
|
|
1260
|
+
// codes (no sort, no reorder). Values are accumulated at a 64-bit width by kind: signed
|
|
1261
|
+
// integers -> int64, unsigned integers -> uint64, floats -> float64; the result uses
|
|
1262
|
+
// that accumulator dtype (the caller casts to a narrower output as needed -- min/max are
|
|
1263
|
+
// selected elements so such a cast is lossless). Integer overflow wraps, matching numpy.
|
|
1264
|
+
// float16/float32 sum/prod are rejected: accumulating them at float64 would not match
|
|
1265
|
+
// numpy's native-width result, so the caller should fall back for those. 'count' returns
|
|
1266
|
+
// int64 group sizes and ignores the values dtype. This is the vectorized replacement for
|
|
1267
|
+
// a per-group Python reduction loop.
|
|
1268
|
+
PyObject *
|
|
1269
|
+
group_reduce(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs)
|
|
1270
|
+
{
|
|
1271
|
+
PyArrayObject *codes = NULL;
|
|
1272
|
+
Py_ssize_t size = 0;
|
|
1273
|
+
PyArrayObject *values = NULL;
|
|
1274
|
+
const char *op_name = NULL;
|
|
1275
|
+
|
|
1276
|
+
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
|
|
1277
|
+
"O!nO!s:group_reduce",
|
|
1278
|
+
group_reduce_kwarg_names,
|
|
1279
|
+
&PyArray_Type, &codes,
|
|
1280
|
+
&size,
|
|
1281
|
+
&PyArray_Type, &values,
|
|
1282
|
+
&op_name
|
|
1283
|
+
)) {
|
|
1284
|
+
return NULL;
|
|
1285
|
+
}
|
|
1286
|
+
AK_GroupReduceOp op;
|
|
1287
|
+
if (AK_group_reduce_op_from_str(op_name, &op)) {
|
|
1288
|
+
return NULL;
|
|
1289
|
+
}
|
|
1290
|
+
if (size < 0) {
|
|
1291
|
+
PyErr_SetString(PyExc_ValueError, "size must be non-negative");
|
|
1292
|
+
return NULL;
|
|
1293
|
+
}
|
|
1294
|
+
if (PyArray_NDIM(codes) != 1 || PyArray_NDIM(values) != 1) {
|
|
1295
|
+
PyErr_SetString(PyExc_ValueError, "Arrays must be 1-dimensional");
|
|
1296
|
+
return NULL;
|
|
1297
|
+
}
|
|
1298
|
+
if (PyArray_TYPE(codes) != NPY_INTP) {
|
|
1299
|
+
PyErr_SetString(PyExc_ValueError, "codes must be of type intp");
|
|
1300
|
+
return NULL;
|
|
1301
|
+
}
|
|
1302
|
+
if (!PyArray_IS_C_CONTIGUOUS(codes) || !PyArray_IS_C_CONTIGUOUS(values)) {
|
|
1303
|
+
PyErr_SetString(PyExc_ValueError, "Arrays must be contiguous");
|
|
1304
|
+
return NULL;
|
|
1305
|
+
}
|
|
1306
|
+
npy_intp n = PyArray_SIZE(codes);
|
|
1307
|
+
if (PyArray_SIZE(values) != n) {
|
|
1308
|
+
PyErr_SetString(PyExc_ValueError,
|
|
1309
|
+
"codes and values must be the same length");
|
|
1310
|
+
return NULL;
|
|
1311
|
+
}
|
|
1312
|
+
const npy_intp *codes_buffer = (npy_intp*)PyArray_DATA(codes);
|
|
1313
|
+
// validate codes are in range before any indexed writes into the output
|
|
1314
|
+
for (npy_intp i = 0; i < n; i++) {
|
|
1315
|
+
npy_intp c = codes_buffer[i];
|
|
1316
|
+
if (c < 0 || c >= size) {
|
|
1317
|
+
PyErr_Format(PyExc_ValueError,
|
|
1318
|
+
"code %zd out of range [0, %zd)",
|
|
1319
|
+
(Py_ssize_t)c, (Py_ssize_t)size);
|
|
1320
|
+
return NULL;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
npy_intp dims[1] = {size};
|
|
1325
|
+
int vtype = PyArray_TYPE(values);
|
|
1326
|
+
|
|
1327
|
+
if (op == GR_COUNT) {
|
|
1328
|
+
PyObject *out_arr = PyArray_ZEROS(1, dims, NPY_INT64, 0);
|
|
1329
|
+
if (!out_arr) {
|
|
1330
|
+
return NULL;
|
|
1331
|
+
}
|
|
1332
|
+
npy_int64 *out = (npy_int64*)PyArray_DATA((PyArrayObject*)out_arr);
|
|
1333
|
+
for (npy_intp i = 0; i < n; i++) {
|
|
1334
|
+
out[codes_buffer[i]]++;
|
|
1335
|
+
}
|
|
1336
|
+
PyArray_CLEARFLAGS((PyArrayObject*)out_arr, NPY_ARRAY_WRITEABLE);
|
|
1337
|
+
return out_arr;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// Choose a 64-bit accumulator by input kind. Integers accumulate exactly at their
|
|
1341
|
+
// signed/unsigned 64-bit width (wrapping on overflow, like numpy); floats at float64.
|
|
1342
|
+
int acc_type;
|
|
1343
|
+
if (PyArray_ISSIGNED(values)) {
|
|
1344
|
+
acc_type = NPY_INT64;
|
|
1345
|
+
}
|
|
1346
|
+
else if (PyArray_ISUNSIGNED(values)) {
|
|
1347
|
+
acc_type = NPY_UINT64;
|
|
1348
|
+
}
|
|
1349
|
+
else if (vtype == NPY_HALF || vtype == NPY_FLOAT || vtype == NPY_DOUBLE) {
|
|
1350
|
+
acc_type = NPY_DOUBLE;
|
|
1351
|
+
}
|
|
1352
|
+
else {
|
|
1353
|
+
PyErr_SetString(PyExc_ValueError,
|
|
1354
|
+
"values must be an integer or float (float16/32/64) dtype");
|
|
1355
|
+
return NULL;
|
|
1356
|
+
}
|
|
1357
|
+
// float16/float32 sum/prod cannot be accumulated at float64 without diverging from
|
|
1358
|
+
// numpy's native-width result; the caller falls back to a per-group reduction.
|
|
1359
|
+
if ((op == GR_SUM || op == GR_PROD)
|
|
1360
|
+
&& acc_type == NPY_DOUBLE && vtype != NPY_DOUBLE) {
|
|
1361
|
+
PyErr_SetString(PyExc_ValueError,
|
|
1362
|
+
"float16/float32 sum/prod is not supported; cast to float64");
|
|
1363
|
+
return NULL;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// upcast the input to the accumulator dtype (no copy when already 64-bit wide)
|
|
1367
|
+
PyArrayObject *v64;
|
|
1368
|
+
if (vtype == acc_type) {
|
|
1369
|
+
Py_INCREF(values);
|
|
1370
|
+
v64 = values;
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
v64 = (PyArrayObject*)PyArray_Cast(values, acc_type);
|
|
1374
|
+
if (v64 == NULL) {
|
|
1375
|
+
return NULL;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
PyObject *out_arr = PyArray_EMPTY(1, dims, acc_type, 0);
|
|
1380
|
+
if (!out_arr) {
|
|
1381
|
+
Py_DECREF(v64);
|
|
1382
|
+
return NULL;
|
|
1383
|
+
}
|
|
1384
|
+
switch (acc_type) {
|
|
1385
|
+
case NPY_DOUBLE:
|
|
1386
|
+
AK_group_reduce_f64(
|
|
1387
|
+
(npy_float64*)PyArray_DATA(v64), codes_buffer, n,
|
|
1388
|
+
(npy_float64*)PyArray_DATA((PyArrayObject*)out_arr), size, op);
|
|
1389
|
+
break;
|
|
1390
|
+
case NPY_UINT64:
|
|
1391
|
+
AK_group_reduce_u64(
|
|
1392
|
+
(npy_uint64*)PyArray_DATA(v64), codes_buffer, n,
|
|
1393
|
+
(npy_uint64*)PyArray_DATA((PyArrayObject*)out_arr), size, op);
|
|
1394
|
+
break;
|
|
1395
|
+
default: // NPY_INT64
|
|
1396
|
+
AK_group_reduce_i64(
|
|
1397
|
+
(npy_int64*)PyArray_DATA(v64), codes_buffer, n,
|
|
1398
|
+
(npy_int64*)PyArray_DATA((PyArrayObject*)out_arr), size, op);
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
Py_DECREF(v64);
|
|
1402
|
+
PyArray_CLEARFLAGS((PyArrayObject*)out_arr, NPY_ARRAY_WRITEABLE);
|
|
1403
|
+
return out_arr;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1131
1406
|
// Fill one strided lane in place: walk positions in the fill direction, carrying
|
|
1132
1407
|
// the most recent non-target value into each target position (subject to `limit`
|
|
1133
1408
|
// consecutive fills per run). `elem_base`/`elem_stride` address elements in bytes;
|
|
@@ -72,6 +72,9 @@ first_true_2d(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs);
|
|
|
72
72
|
PyObject *
|
|
73
73
|
group_ordering(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs);
|
|
74
74
|
|
|
75
|
+
PyObject *
|
|
76
|
+
group_reduce(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs);
|
|
77
|
+
|
|
75
78
|
PyObject *
|
|
76
79
|
fill_directional(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs);
|
|
77
80
|
|
|
@@ -310,6 +310,60 @@ TriMap_register_many_from_one(TriMapObject *self, PyObject *arg) {
|
|
|
310
310
|
Py_RETURN_NONE;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
// Bulk pair registration: given two equal-length int64 arrays `src_pos` and `dst_pos`,
|
|
314
|
+
// register the pair (src_pos[i], dst_pos[i]) for each i in a single C loop -- equivalent
|
|
315
|
+
// to calling register_one(src_pos[i], dst_pos[i]) for each i, but without per-element
|
|
316
|
+
// Python overhead. Either position may be -1 to indicate an unmatched (fill) side. This
|
|
317
|
+
// generalizes register_many_from_one (where src is the implicit range 0..src_len) to
|
|
318
|
+
// arbitrary src positions, as needed for many-to-many joins.
|
|
319
|
+
PyObject *
|
|
320
|
+
TriMap_register_pairs(TriMapObject *self, PyObject *args) {
|
|
321
|
+
PyObject* src_arg;
|
|
322
|
+
PyObject* dst_arg;
|
|
323
|
+
if (!PyArg_ParseTuple(args,
|
|
324
|
+
"OO:register_pairs",
|
|
325
|
+
&src_arg,
|
|
326
|
+
&dst_arg)) {
|
|
327
|
+
return NULL;
|
|
328
|
+
}
|
|
329
|
+
if (self->finalized) {
|
|
330
|
+
PyErr_SetString(PyExc_RuntimeError, "Cannot register post finalization");
|
|
331
|
+
return NULL;
|
|
332
|
+
}
|
|
333
|
+
if (!PyArray_Check(src_arg) || !PyArray_Check(dst_arg)) {
|
|
334
|
+
PyErr_SetString(PyExc_TypeError, "Must provide arrays");
|
|
335
|
+
return NULL;
|
|
336
|
+
}
|
|
337
|
+
PyArrayObject* src_a = (PyArrayObject*)src_arg;
|
|
338
|
+
PyArrayObject* dst_a = (PyArrayObject*)dst_arg;
|
|
339
|
+
if (PyArray_TYPE(src_a) != NPY_INT64 || PyArray_TYPE(dst_a) != NPY_INT64) {
|
|
340
|
+
PyErr_SetString(PyExc_ValueError, "Arrays must be of type int64");
|
|
341
|
+
return NULL;
|
|
342
|
+
}
|
|
343
|
+
if (PyArray_NDIM(src_a) != 1 || PyArray_NDIM(dst_a) != 1) {
|
|
344
|
+
PyErr_SetString(PyExc_ValueError, "Arrays must be 1-dimensional");
|
|
345
|
+
return NULL;
|
|
346
|
+
}
|
|
347
|
+
if (!PyArray_IS_C_CONTIGUOUS(src_a) || !PyArray_ISALIGNED(src_a)
|
|
348
|
+
|| !PyArray_IS_C_CONTIGUOUS(dst_a) || !PyArray_ISALIGNED(dst_a)) {
|
|
349
|
+
PyErr_SetString(PyExc_ValueError, "Arrays must be contiguous");
|
|
350
|
+
return NULL;
|
|
351
|
+
}
|
|
352
|
+
npy_intp n = PyArray_SIZE(src_a);
|
|
353
|
+
if (n != PyArray_SIZE(dst_a)) {
|
|
354
|
+
PyErr_SetString(PyExc_ValueError, "Arrays must be the same length");
|
|
355
|
+
return NULL;
|
|
356
|
+
}
|
|
357
|
+
const npy_int64* s = (npy_int64*)PyArray_DATA(src_a);
|
|
358
|
+
const npy_int64* d = (npy_int64*)PyArray_DATA(dst_a);
|
|
359
|
+
for (npy_intp i = 0; i < n; i++) {
|
|
360
|
+
if (AK_TM_register_one(self, (Py_ssize_t)s[i], (Py_ssize_t)d[i])) {
|
|
361
|
+
return NULL;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
Py_RETURN_NONE;
|
|
365
|
+
}
|
|
366
|
+
|
|
313
367
|
PyObject *
|
|
314
368
|
TriMap_register_unmatched_dst(TriMapObject *self) {
|
|
315
369
|
if (self->finalized) {
|
|
@@ -1400,6 +1454,7 @@ TriMap_map_dst_fill(TriMapObject *self, PyObject *args) {
|
|
|
1400
1454
|
static PyMethodDef TriMap_methods[] = {
|
|
1401
1455
|
{"register_one", (PyCFunction)TriMap_register_one, METH_VARARGS, NULL},
|
|
1402
1456
|
{"register_many_from_one", (PyCFunction)TriMap_register_many_from_one, METH_O, NULL},
|
|
1457
|
+
{"register_pairs", (PyCFunction)TriMap_register_pairs, METH_VARARGS, NULL},
|
|
1403
1458
|
{"register_unmatched_dst", (PyCFunction)TriMap_register_unmatched_dst, METH_NOARGS, NULL},
|
|
1404
1459
|
{"register_many", (PyCFunction)TriMap_register_many, METH_VARARGS, NULL},
|
|
1405
1460
|
{"finalize", (PyCFunction)TriMap_finalize, METH_NOARGS, NULL},
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from arraykit import factorize, group_reduce
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestUnit(unittest.TestCase):
|
|
8
|
+
# ------------------------------------------------------------------
|
|
9
|
+
# basic behavior
|
|
10
|
+
|
|
11
|
+
def test_group_reduce_sum_f64(self) -> None:
|
|
12
|
+
codes = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp)
|
|
13
|
+
values = np.array([1.0, 10.0, 2.0, 100.0, 20.0, 3.0])
|
|
14
|
+
post = group_reduce(codes, 3, values, 'sum')
|
|
15
|
+
self.assertEqual(post.tolist(), [6.0, 30.0, 100.0])
|
|
16
|
+
self.assertEqual(post.dtype, np.dtype(np.float64))
|
|
17
|
+
|
|
18
|
+
def test_group_reduce_all_ops_f64(self) -> None:
|
|
19
|
+
codes = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp)
|
|
20
|
+
values = np.array([1.0, 10.0, 2.0, 100.0, 20.0, 3.0])
|
|
21
|
+
self.assertEqual(group_reduce(codes, 3, values, 'sum').tolist(), [6.0, 30.0, 100.0])
|
|
22
|
+
self.assertEqual(group_reduce(codes, 3, values, 'prod').tolist(), [6.0, 200.0, 100.0])
|
|
23
|
+
self.assertEqual(group_reduce(codes, 3, values, 'min').tolist(), [1.0, 10.0, 100.0])
|
|
24
|
+
self.assertEqual(group_reduce(codes, 3, values, 'max').tolist(), [3.0, 20.0, 100.0])
|
|
25
|
+
|
|
26
|
+
def test_group_reduce_all_ops_i64(self) -> None:
|
|
27
|
+
codes = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp)
|
|
28
|
+
values = np.array([1, 10, 2, 100, 20, 3], dtype=np.int64)
|
|
29
|
+
for op in ('sum', 'prod', 'min', 'max'):
|
|
30
|
+
post = group_reduce(codes, 3, values, op)
|
|
31
|
+
self.assertEqual(post.dtype, np.dtype(np.int64))
|
|
32
|
+
self.assertEqual(group_reduce(codes, 3, values, 'sum').tolist(), [6, 30, 100])
|
|
33
|
+
self.assertEqual(group_reduce(codes, 3, values, 'prod').tolist(), [6, 200, 100])
|
|
34
|
+
self.assertEqual(group_reduce(codes, 3, values, 'min').tolist(), [1, 10, 100])
|
|
35
|
+
self.assertEqual(group_reduce(codes, 3, values, 'max').tolist(), [3, 20, 100])
|
|
36
|
+
|
|
37
|
+
def test_group_reduce_all_int_widths(self) -> None:
|
|
38
|
+
# every signed width accumulates at int64; every unsigned width at uint64.
|
|
39
|
+
# values are exact (selected elements / small sums), only the dtype widens.
|
|
40
|
+
codes = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp)
|
|
41
|
+
raw = [1, 10, 2, 100, 20, 3]
|
|
42
|
+
for dt in ('int8', 'int16', 'int32', 'int64'):
|
|
43
|
+
values = np.array(raw, dtype=dt)
|
|
44
|
+
for op, exp in (
|
|
45
|
+
('sum', [6, 30, 100]),
|
|
46
|
+
('min', [1, 10, 100]),
|
|
47
|
+
('max', [3, 20, 100]),
|
|
48
|
+
):
|
|
49
|
+
post = group_reduce(codes, 3, values, op)
|
|
50
|
+
self.assertEqual(post.dtype, np.dtype(np.int64), (dt, op))
|
|
51
|
+
self.assertEqual(post.tolist(), exp, (dt, op))
|
|
52
|
+
for dt in ('uint8', 'uint16', 'uint32', 'uint64'):
|
|
53
|
+
values = np.array(raw, dtype=dt)
|
|
54
|
+
for op, exp in (
|
|
55
|
+
('sum', [6, 30, 100]),
|
|
56
|
+
('min', [1, 10, 100]),
|
|
57
|
+
('max', [3, 20, 100]),
|
|
58
|
+
):
|
|
59
|
+
post = group_reduce(codes, 3, values, op)
|
|
60
|
+
self.assertEqual(post.dtype, np.dtype(np.uint64), (dt, op))
|
|
61
|
+
self.assertEqual(post.tolist(), exp, (dt, op))
|
|
62
|
+
|
|
63
|
+
def test_group_reduce_float_widths(self) -> None:
|
|
64
|
+
# min/max/count work for every float width (result is float64); the
|
|
65
|
+
# selected element is exact
|
|
66
|
+
codes = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp)
|
|
67
|
+
raw = [1.0, 10.0, 2.0, 100.0, 20.0, 3.0]
|
|
68
|
+
for dt in ('float16', 'float32', 'float64'):
|
|
69
|
+
values = np.array(raw, dtype=dt)
|
|
70
|
+
for op, exp in (('min', [1.0, 10.0, 100.0]), ('max', [3.0, 20.0, 100.0])):
|
|
71
|
+
post = group_reduce(codes, 3, values, op)
|
|
72
|
+
self.assertEqual(post.dtype, np.dtype(np.float64), (dt, op))
|
|
73
|
+
self.assertEqual(post.tolist(), exp, (dt, op))
|
|
74
|
+
|
|
75
|
+
def test_group_reduce_narrow_float_sum_rejected(self) -> None:
|
|
76
|
+
# float16/float32 sum/prod cannot match numpy's native-width result at
|
|
77
|
+
# float64, so they are rejected (caller falls back); float64 is fine
|
|
78
|
+
codes = np.array([0, 1], dtype=np.intp)
|
|
79
|
+
for dt in ('float16', 'float32'):
|
|
80
|
+
values = np.array([1.0, 2.0], dtype=dt)
|
|
81
|
+
for op in ('sum', 'prod'):
|
|
82
|
+
with self.assertRaises(ValueError):
|
|
83
|
+
group_reduce(codes, 2, values, op)
|
|
84
|
+
# float64 sum/prod is supported
|
|
85
|
+
self.assertEqual(
|
|
86
|
+
group_reduce(codes, 2, np.array([1.0, 2.0]), 'sum').tolist(), [1.0, 2.0]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def test_group_reduce_integer_overflow_wraps(self) -> None:
|
|
90
|
+
# integer sum overflow wraps silently, matching numpy (not raising)
|
|
91
|
+
codes = np.array([0, 0], dtype=np.intp)
|
|
92
|
+
big = np.full(2, np.iinfo(np.int64).max, dtype=np.int64)
|
|
93
|
+
self.assertEqual(
|
|
94
|
+
group_reduce(codes, 1, big, 'sum').tolist(), [int(np.sum(big))]
|
|
95
|
+
)
|
|
96
|
+
ubig = np.full(2, np.iinfo(np.uint64).max, dtype=np.uint64)
|
|
97
|
+
self.assertEqual(
|
|
98
|
+
group_reduce(codes, 1, ubig, 'sum').tolist(), [int(np.sum(ubig))]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def test_group_reduce_count(self) -> None:
|
|
102
|
+
# count returns int64 group sizes regardless of values dtype
|
|
103
|
+
codes = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp)
|
|
104
|
+
post = group_reduce(codes, 3, np.array([1.0, 2, 3, 4, 5, 6]), 'count')
|
|
105
|
+
self.assertEqual(post.tolist(), [3, 2, 1])
|
|
106
|
+
self.assertEqual(post.dtype, np.dtype(np.int64))
|
|
107
|
+
# count ignores the values dtype entirely
|
|
108
|
+
post = group_reduce(codes, 3, np.array([1, 2, 3, 4, 5, 6], dtype=np.int64), 'count')
|
|
109
|
+
self.assertEqual(post.tolist(), [3, 2, 1])
|
|
110
|
+
|
|
111
|
+
def test_group_reduce_nan_propagates(self) -> None:
|
|
112
|
+
# min/max propagate NaN, matching np.min/np.max (not the nan-skipping variants)
|
|
113
|
+
codes = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp)
|
|
114
|
+
values = np.array([1.0, np.nan, 2.0, 5.0, np.nan, 3.0])
|
|
115
|
+
mx = group_reduce(codes, 3, values, 'max')
|
|
116
|
+
mn = group_reduce(codes, 3, values, 'min')
|
|
117
|
+
self.assertEqual(mx[0], 3.0)
|
|
118
|
+
self.assertTrue(np.isnan(mx[1]))
|
|
119
|
+
self.assertEqual(mx[2], 5.0)
|
|
120
|
+
self.assertEqual(mn[0], 1.0)
|
|
121
|
+
self.assertTrue(np.isnan(mn[1]))
|
|
122
|
+
# sum also propagates NaN
|
|
123
|
+
s = group_reduce(codes, 3, values, 'sum')
|
|
124
|
+
self.assertTrue(np.isnan(s[1]))
|
|
125
|
+
|
|
126
|
+
def test_group_reduce_single_group(self) -> None:
|
|
127
|
+
codes = np.array([0, 0, 0], dtype=np.intp)
|
|
128
|
+
values = np.array([1.0, 2.0, 3.0])
|
|
129
|
+
self.assertEqual(group_reduce(codes, 1, values, 'sum').tolist(), [6.0])
|
|
130
|
+
|
|
131
|
+
def test_group_reduce_empty(self) -> None:
|
|
132
|
+
codes = np.array([], dtype=np.intp)
|
|
133
|
+
values = np.array([], dtype=np.float64)
|
|
134
|
+
self.assertEqual(group_reduce(codes, 0, values, 'sum').tolist(), [])
|
|
135
|
+
|
|
136
|
+
def test_group_reduce_outputs_immutable(self) -> None:
|
|
137
|
+
codes = np.array([0, 1, 0], dtype=np.intp)
|
|
138
|
+
values = np.array([1.0, 2.0, 3.0])
|
|
139
|
+
for op in ('sum', 'prod', 'min', 'max', 'count'):
|
|
140
|
+
post = group_reduce(codes, 2, values, op)
|
|
141
|
+
self.assertFalse(post.flags.writeable)
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
# equivalence to a per-group numpy reduction
|
|
145
|
+
|
|
146
|
+
def test_group_reduce_equivalence_f64(self) -> None:
|
|
147
|
+
rng = np.random.RandomState(0)
|
|
148
|
+
for _ in range(20):
|
|
149
|
+
size = int(rng.randint(1, 40))
|
|
150
|
+
n = int(rng.randint(size, size + 500))
|
|
151
|
+
codes = rng.randint(0, size, n).astype(np.intp)
|
|
152
|
+
values = rng.rand(n) * 100
|
|
153
|
+
for op, npf in (('sum', np.sum), ('min', np.min), ('max', np.max)):
|
|
154
|
+
got = group_reduce(codes, size, values, op)
|
|
155
|
+
for g in range(size):
|
|
156
|
+
mask = codes == g
|
|
157
|
+
if np.any(mask): # real usage (factorize) has no empty groups
|
|
158
|
+
self.assertTrue(np.isclose(got[g], npf(values[mask])), op)
|
|
159
|
+
|
|
160
|
+
def test_group_reduce_equivalence_i64(self) -> None:
|
|
161
|
+
rng = np.random.RandomState(1)
|
|
162
|
+
for _ in range(20):
|
|
163
|
+
size = int(rng.randint(1, 40))
|
|
164
|
+
n = int(rng.randint(size, size + 500))
|
|
165
|
+
codes = rng.randint(0, size, n).astype(np.intp)
|
|
166
|
+
values = rng.randint(-1000, 1000, n).astype(np.int64)
|
|
167
|
+
for op, npf in (('sum', np.sum), ('min', np.min), ('max', np.max)):
|
|
168
|
+
got = group_reduce(codes, size, values, op)
|
|
169
|
+
for g in range(size):
|
|
170
|
+
mask = codes == g
|
|
171
|
+
if np.any(mask):
|
|
172
|
+
self.assertEqual(got[g], npf(values[mask]), op)
|
|
173
|
+
|
|
174
|
+
def test_group_reduce_with_factorize(self) -> None:
|
|
175
|
+
# the intended pipeline: factorize(sort=True) -> group_reduce
|
|
176
|
+
key = np.array([30, 10, 20, 10, 30, 20, 10])
|
|
177
|
+
values = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0])
|
|
178
|
+
uniques, codes = factorize(key, sort=True)
|
|
179
|
+
self.assertEqual(uniques.tolist(), [10, 20, 30])
|
|
180
|
+
post = group_reduce(codes, len(uniques), values, 'sum')
|
|
181
|
+
# group 10 -> 2+4+7=13; group 20 -> 3+6=9; group 30 -> 1+5=6
|
|
182
|
+
self.assertEqual(post.tolist(), [13.0, 9.0, 6.0])
|
|
183
|
+
|
|
184
|
+
# ------------------------------------------------------------------
|
|
185
|
+
# errors
|
|
186
|
+
|
|
187
|
+
def test_group_reduce_errors(self) -> None:
|
|
188
|
+
codes = np.array([0, 1, 0], dtype=np.intp)
|
|
189
|
+
values = np.array([1.0, 2.0, 3.0])
|
|
190
|
+
with self.assertRaises(ValueError): # unknown op
|
|
191
|
+
group_reduce(codes, 2, values, 'median')
|
|
192
|
+
with self.assertRaises(ValueError): # length mismatch
|
|
193
|
+
group_reduce(codes, 2, np.array([1.0, 2.0]), 'sum')
|
|
194
|
+
with self.assertRaises(ValueError): # code out of range
|
|
195
|
+
group_reduce(np.array([0, 5], dtype=np.intp), 2, np.array([1.0, 2.0]), 'sum')
|
|
196
|
+
with self.assertRaises(ValueError): # negative code
|
|
197
|
+
group_reduce(np.array([0, -1], dtype=np.intp), 2, np.array([1.0, 2.0]), 'sum')
|
|
198
|
+
with self.assertRaises(ValueError): # codes wrong dtype (int8 is never intp)
|
|
199
|
+
group_reduce(np.array([0, 1], dtype=np.int8), 2, np.array([1.0, 2.0]), 'sum')
|
|
200
|
+
with self.assertRaises(ValueError): # values unsupported dtype (complex)
|
|
201
|
+
group_reduce(codes, 2, np.array([1, 2, 3], dtype=np.complex128), 'sum')
|
|
202
|
+
with self.assertRaises(ValueError): # values unsupported dtype (datetime)
|
|
203
|
+
group_reduce(
|
|
204
|
+
codes, 2, np.array([1, 2, 3], dtype='datetime64[s]'), 'max'
|
|
205
|
+
)
|
|
206
|
+
with self.assertRaises(ValueError): # negative size
|
|
207
|
+
group_reduce(codes, -1, values, 'sum')
|
|
208
|
+
with self.assertRaises(ValueError): # 2d codes
|
|
209
|
+
group_reduce(
|
|
210
|
+
np.array([[0, 1]], dtype=np.intp), 2, np.array([[1.0, 2.0]]), 'sum'
|
|
211
|
+
)
|
|
212
|
+
with self.assertRaises(TypeError): # not an array
|
|
213
|
+
group_reduce([0, 1, 0], 2, values, 'sum')
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
if __name__ == '__main__':
|
|
217
|
+
unittest.main()
|
|
@@ -1508,3 +1508,157 @@ class TestUnit(unittest.TestCase):
|
|
|
1508
1508
|
tm.finalize()
|
|
1509
1509
|
with self.assertRaises(RuntimeError): # post-finalize
|
|
1510
1510
|
tm.register_many_from_one(np.array([0, 1], dtype=np.int64))
|
|
1511
|
+
|
|
1512
|
+
# ---------------------------------------------------------------------------
|
|
1513
|
+
# register_pairs (bulk arbitrary (src, dst) pairs)
|
|
1514
|
+
|
|
1515
|
+
def test_tri_map_register_pairs_a(self) -> None:
|
|
1516
|
+
# a many-to-many mapping: src 0 -> dst 0 and 1; src 1 -> dst 1
|
|
1517
|
+
tm = TriMap(2, 2)
|
|
1518
|
+
tm.register_pairs(
|
|
1519
|
+
np.array([0, 0, 1], dtype=np.int64),
|
|
1520
|
+
np.array([0, 1, 1], dtype=np.int64),
|
|
1521
|
+
)
|
|
1522
|
+
tm.finalize()
|
|
1523
|
+
self.assertTrue(tm.is_many())
|
|
1524
|
+
self.assertTrue(tm.src_no_fill())
|
|
1525
|
+
self.assertTrue(tm.dst_no_fill())
|
|
1526
|
+
src = np.array([10, 20])
|
|
1527
|
+
dst = np.array([100, 200])
|
|
1528
|
+
self.assertEqual(tm.map_src_no_fill(src).tolist(), [10, 10, 20])
|
|
1529
|
+
self.assertEqual(tm.map_dst_no_fill(dst).tolist(), [100, 200, 200])
|
|
1530
|
+
|
|
1531
|
+
def test_tri_map_register_pairs_unmatched(self) -> None:
|
|
1532
|
+
# -1 on either side marks that side as a fill row
|
|
1533
|
+
tm = TriMap(3, 2)
|
|
1534
|
+
tm.register_pairs(
|
|
1535
|
+
np.array([0, 1, 2], dtype=np.int64),
|
|
1536
|
+
np.array([0, 1, -1], dtype=np.int64),
|
|
1537
|
+
)
|
|
1538
|
+
tm.finalize()
|
|
1539
|
+
self.assertTrue(tm.src_no_fill()) # every row has a src
|
|
1540
|
+
self.assertFalse(tm.dst_no_fill()) # row 2 has no dst
|
|
1541
|
+
dst = np.array([100, 200])
|
|
1542
|
+
self.assertEqual(
|
|
1543
|
+
tm.map_dst_fill(dst, -9, np.dtype(np.int64)).tolist(), [100, 200, -9]
|
|
1544
|
+
)
|
|
1545
|
+
|
|
1546
|
+
def test_tri_map_register_pairs_unmatched_src(self) -> None:
|
|
1547
|
+
# -1 on the src side (as register_unmatched_dst would produce for OUTER)
|
|
1548
|
+
tm = TriMap(2, 3)
|
|
1549
|
+
tm.register_pairs(
|
|
1550
|
+
np.array([0, 1, -1], dtype=np.int64),
|
|
1551
|
+
np.array([0, 1, 2], dtype=np.int64),
|
|
1552
|
+
)
|
|
1553
|
+
tm.finalize()
|
|
1554
|
+
self.assertFalse(tm.src_no_fill()) # row 2 has no src
|
|
1555
|
+
self.assertTrue(tm.dst_no_fill()) # every row has a dst
|
|
1556
|
+
src = np.array([10, 20])
|
|
1557
|
+
self.assertEqual(
|
|
1558
|
+
tm.map_src_fill(src, -9, np.dtype(np.int64)).tolist(), [10, 20, -9]
|
|
1559
|
+
)
|
|
1560
|
+
|
|
1561
|
+
def test_tri_map_register_pairs_is_many(self) -> None:
|
|
1562
|
+
# a single output row per src/dst -> not many
|
|
1563
|
+
tm = TriMap(2, 2)
|
|
1564
|
+
tm.register_pairs(
|
|
1565
|
+
np.array([0, 1], dtype=np.int64),
|
|
1566
|
+
np.array([1, 0], dtype=np.int64),
|
|
1567
|
+
)
|
|
1568
|
+
tm.finalize()
|
|
1569
|
+
self.assertFalse(tm.is_many())
|
|
1570
|
+
|
|
1571
|
+
def test_tri_map_register_pairs_equivalence_loop(self) -> None:
|
|
1572
|
+
# register_pairs == a register_one loop over the same pairs
|
|
1573
|
+
rng = np.random.RandomState(0)
|
|
1574
|
+
for _ in range(50):
|
|
1575
|
+
src_len = int(rng.randint(1, 12))
|
|
1576
|
+
dst_len = int(rng.randint(1, 10))
|
|
1577
|
+
n = int(rng.randint(1, 20))
|
|
1578
|
+
src_pos = rng.randint(-1, src_len, size=n).astype(np.int64)
|
|
1579
|
+
dst_pos = rng.randint(-1, dst_len, size=n).astype(np.int64)
|
|
1580
|
+
tb = TriMap(src_len, dst_len)
|
|
1581
|
+
tb.register_pairs(src_pos, dst_pos)
|
|
1582
|
+
tb.finalize()
|
|
1583
|
+
tl = TriMap(src_len, dst_len)
|
|
1584
|
+
for i in range(n):
|
|
1585
|
+
tl.register_one(int(src_pos[i]), int(dst_pos[i]))
|
|
1586
|
+
tl.finalize()
|
|
1587
|
+
src = np.arange(100, 100 + src_len)
|
|
1588
|
+
dst = np.arange(200, 200 + dst_len)
|
|
1589
|
+
self.assertEqual(tb.is_many(), tl.is_many())
|
|
1590
|
+
self.assertEqual(
|
|
1591
|
+
tb.map_src_fill(src, -1, np.dtype(np.int64)).tolist(),
|
|
1592
|
+
tl.map_src_fill(src, -1, np.dtype(np.int64)).tolist(),
|
|
1593
|
+
)
|
|
1594
|
+
self.assertEqual(
|
|
1595
|
+
tb.map_dst_fill(dst, -1, np.dtype(np.int64)).tolist(),
|
|
1596
|
+
tl.map_dst_fill(dst, -1, np.dtype(np.int64)).tolist(),
|
|
1597
|
+
)
|
|
1598
|
+
|
|
1599
|
+
def test_tri_map_register_pairs_equivalence_many_from_one(self) -> None:
|
|
1600
|
+
# with src = arange(src_len), register_pairs matches register_many_from_one
|
|
1601
|
+
rng = np.random.RandomState(1)
|
|
1602
|
+
for _ in range(20):
|
|
1603
|
+
src_len = int(rng.randint(1, 12))
|
|
1604
|
+
dst_len = int(rng.randint(1, 10))
|
|
1605
|
+
dst_pos = rng.randint(-1, dst_len, size=src_len).astype(np.int64)
|
|
1606
|
+
src_pos = np.arange(src_len, dtype=np.int64)
|
|
1607
|
+
tp = TriMap(src_len, dst_len)
|
|
1608
|
+
tp.register_pairs(src_pos, dst_pos)
|
|
1609
|
+
tp.finalize()
|
|
1610
|
+
tm = TriMap(src_len, dst_len)
|
|
1611
|
+
tm.register_many_from_one(dst_pos)
|
|
1612
|
+
tm.finalize()
|
|
1613
|
+
src = np.arange(100, 100 + src_len)
|
|
1614
|
+
dst = np.arange(200, 200 + dst_len)
|
|
1615
|
+
self.assertEqual(tp.is_many(), tm.is_many())
|
|
1616
|
+
self.assertEqual(
|
|
1617
|
+
tp.map_src_fill(src, -1, np.dtype(np.int64)).tolist(),
|
|
1618
|
+
tm.map_src_fill(src, -1, np.dtype(np.int64)).tolist(),
|
|
1619
|
+
)
|
|
1620
|
+
self.assertEqual(
|
|
1621
|
+
tp.map_dst_fill(dst, -1, np.dtype(np.int64)).tolist(),
|
|
1622
|
+
tm.map_dst_fill(dst, -1, np.dtype(np.int64)).tolist(),
|
|
1623
|
+
)
|
|
1624
|
+
|
|
1625
|
+
def test_tri_map_register_pairs_empty(self) -> None:
|
|
1626
|
+
tm = TriMap(3, 3)
|
|
1627
|
+
tm.register_pairs(
|
|
1628
|
+
np.array([], dtype=np.int64), np.array([], dtype=np.int64)
|
|
1629
|
+
)
|
|
1630
|
+
tm.finalize()
|
|
1631
|
+
self.assertEqual(tm.map_src_no_fill(np.arange(3)).tolist(), [])
|
|
1632
|
+
|
|
1633
|
+
def test_tri_map_register_pairs_errors(self) -> None:
|
|
1634
|
+
with self.assertRaises(ValueError): # length mismatch
|
|
1635
|
+
TriMap(3, 3).register_pairs(
|
|
1636
|
+
np.array([0, 1], dtype=np.int64), np.array([0], dtype=np.int64)
|
|
1637
|
+
)
|
|
1638
|
+
with self.assertRaises(ValueError): # wrong dtype
|
|
1639
|
+
TriMap(3, 3).register_pairs(
|
|
1640
|
+
np.array([0, 1], dtype=np.int32), np.array([0, 1], dtype=np.int64)
|
|
1641
|
+
)
|
|
1642
|
+
with self.assertRaises(ValueError): # 2d
|
|
1643
|
+
TriMap(3, 3).register_pairs(
|
|
1644
|
+
np.array([[0, 1]], dtype=np.int64), np.array([[0, 1]], dtype=np.int64)
|
|
1645
|
+
)
|
|
1646
|
+
with self.assertRaises(ValueError): # out of bounds src
|
|
1647
|
+
TriMap(3, 3).register_pairs(
|
|
1648
|
+
np.array([9], dtype=np.int64), np.array([0], dtype=np.int64)
|
|
1649
|
+
)
|
|
1650
|
+
with self.assertRaises(ValueError): # out of bounds dst
|
|
1651
|
+
TriMap(3, 3).register_pairs(
|
|
1652
|
+
np.array([0], dtype=np.int64), np.array([9], dtype=np.int64)
|
|
1653
|
+
)
|
|
1654
|
+
with self.assertRaises(TypeError): # not an array
|
|
1655
|
+
TriMap(3, 3).register_pairs([0, 1], np.array([0, 1], dtype=np.int64))
|
|
1656
|
+
tm = TriMap(2, 2)
|
|
1657
|
+
tm.register_pairs(
|
|
1658
|
+
np.array([0, 1], dtype=np.int64), np.array([0, 1], dtype=np.int64)
|
|
1659
|
+
)
|
|
1660
|
+
tm.finalize()
|
|
1661
|
+
with self.assertRaises(RuntimeError): # post-finalize
|
|
1662
|
+
tm.register_pairs(
|
|
1663
|
+
np.array([0], dtype=np.int64), np.array([0], dtype=np.int64)
|
|
1664
|
+
)
|
arraykit-1.9.0/VERSION
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|