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.
Files changed (54) hide show
  1. {arraykit-1.9.0/arraykit.egg-info → arraykit-1.11.0}/PKG-INFO +13 -1
  2. {arraykit-1.9.0 → arraykit-1.11.0}/README.rst +12 -0
  3. arraykit-1.11.0/VERSION +2 -0
  4. {arraykit-1.9.0 → arraykit-1.11.0/arraykit.egg-info}/PKG-INFO +13 -1
  5. {arraykit-1.9.0 → arraykit-1.11.0}/arraykit.egg-info/SOURCES.txt +1 -0
  6. {arraykit-1.9.0 → arraykit-1.11.0}/src/__init__.py +1 -0
  7. {arraykit-1.9.0 → arraykit-1.11.0}/src/__init__.pyi +4 -0
  8. {arraykit-1.9.0 → arraykit-1.11.0}/src/_arraykit.c +4 -0
  9. {arraykit-1.9.0 → arraykit-1.11.0}/src/methods.c +275 -0
  10. {arraykit-1.9.0 → arraykit-1.11.0}/src/methods.h +3 -0
  11. {arraykit-1.9.0 → arraykit-1.11.0}/src/tri_map.c +55 -0
  12. arraykit-1.11.0/test/test_group_reduce.py +217 -0
  13. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_tri_map.py +154 -0
  14. arraykit-1.9.0/VERSION +0 -2
  15. {arraykit-1.9.0 → arraykit-1.11.0}/LICENSE.txt +0 -0
  16. {arraykit-1.9.0 → arraykit-1.11.0}/MANIFEST.in +0 -0
  17. {arraykit-1.9.0 → arraykit-1.11.0}/arraykit.egg-info/dependency_links.txt +0 -0
  18. {arraykit-1.9.0 → arraykit-1.11.0}/arraykit.egg-info/requires.txt +0 -0
  19. {arraykit-1.9.0 → arraykit-1.11.0}/arraykit.egg-info/top_level.txt +0 -0
  20. {arraykit-1.9.0 → arraykit-1.11.0}/pyproject.toml +0 -0
  21. {arraykit-1.9.0 → arraykit-1.11.0}/setup.cfg +0 -0
  22. {arraykit-1.9.0 → arraykit-1.11.0}/setup.py +0 -0
  23. {arraykit-1.9.0 → arraykit-1.11.0}/src/array_go.c +0 -0
  24. {arraykit-1.9.0 → arraykit-1.11.0}/src/array_go.h +0 -0
  25. {arraykit-1.9.0 → arraykit-1.11.0}/src/array_to_tuple.c +0 -0
  26. {arraykit-1.9.0 → arraykit-1.11.0}/src/array_to_tuple.h +0 -0
  27. {arraykit-1.9.0 → arraykit-1.11.0}/src/auto_map.c +0 -0
  28. {arraykit-1.9.0 → arraykit-1.11.0}/src/auto_map.h +0 -0
  29. {arraykit-1.9.0 → arraykit-1.11.0}/src/block_index.c +0 -0
  30. {arraykit-1.9.0 → arraykit-1.11.0}/src/block_index.h +0 -0
  31. {arraykit-1.9.0 → arraykit-1.11.0}/src/delimited_to_arrays.c +0 -0
  32. {arraykit-1.9.0 → arraykit-1.11.0}/src/delimited_to_arrays.h +0 -0
  33. {arraykit-1.9.0 → arraykit-1.11.0}/src/py.typed +0 -0
  34. {arraykit-1.9.0 → arraykit-1.11.0}/src/tri_map.h +0 -0
  35. {arraykit-1.9.0 → arraykit-1.11.0}/src/utilities.h +0 -0
  36. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_array_go.py +0 -0
  37. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_astype_array.py +0 -0
  38. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_auto_map.py +0 -0
  39. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_auto_map_property.py +0 -0
  40. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_block_index.py +0 -0
  41. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_delimited_to_arrays.py +0 -0
  42. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_delimited_to_arrays_integration.py +0 -0
  43. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_delimited_to_arrays_property.py +0 -0
  44. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_factorize.py +0 -0
  45. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_fill_directional.py +0 -0
  46. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_group_ordering.py +0 -0
  47. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_nonzero_1d.py +0 -0
  48. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_nonzero_1d_property.py +0 -0
  49. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_objectable.py +0 -0
  50. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_pyi.py +0 -0
  51. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_split_after_count.py +0 -0
  52. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_transition_slices_from_group.py +0 -0
  53. {arraykit-1.9.0 → arraykit-1.11.0}/test/test_type_discovery.py +0 -0
  54. {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.9.0
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
 
@@ -0,0 +1,2 @@
1
+ 1.11.0
2
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arraykit
3
- Version: 1.9.0
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
 
@@ -39,6 +39,7 @@ test/test_delimited_to_arrays_property.py
39
39
  test/test_factorize.py
40
40
  test/test_fill_directional.py
41
41
  test/test_group_ordering.py
42
+ test/test_group_reduce.py
42
43
  test/test_nonzero_1d.py
43
44
  test/test_nonzero_1d_property.py
44
45
  test/test_objectable.py
@@ -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
@@ -1,2 +0,0 @@
1
- 1.9.0
2
-
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