arraykit 1.8.0__tar.gz → 1.10.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 (53) hide show
  1. {arraykit-1.8.0/arraykit.egg-info → arraykit-1.10.0}/PKG-INFO +13 -1
  2. {arraykit-1.8.0 → arraykit-1.10.0}/README.rst +12 -0
  3. arraykit-1.10.0/VERSION +2 -0
  4. {arraykit-1.8.0 → arraykit-1.10.0/arraykit.egg-info}/PKG-INFO +13 -1
  5. {arraykit-1.8.0 → arraykit-1.10.0}/src/__init__.pyi +3 -0
  6. {arraykit-1.8.0 → arraykit-1.10.0}/src/auto_map.c +204 -0
  7. {arraykit-1.8.0 → arraykit-1.10.0}/src/tri_map.c +97 -0
  8. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_auto_map.py +103 -0
  9. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_tri_map.py +223 -0
  10. arraykit-1.8.0/VERSION +0 -2
  11. {arraykit-1.8.0 → arraykit-1.10.0}/LICENSE.txt +0 -0
  12. {arraykit-1.8.0 → arraykit-1.10.0}/MANIFEST.in +0 -0
  13. {arraykit-1.8.0 → arraykit-1.10.0}/arraykit.egg-info/SOURCES.txt +0 -0
  14. {arraykit-1.8.0 → arraykit-1.10.0}/arraykit.egg-info/dependency_links.txt +0 -0
  15. {arraykit-1.8.0 → arraykit-1.10.0}/arraykit.egg-info/requires.txt +0 -0
  16. {arraykit-1.8.0 → arraykit-1.10.0}/arraykit.egg-info/top_level.txt +0 -0
  17. {arraykit-1.8.0 → arraykit-1.10.0}/pyproject.toml +0 -0
  18. {arraykit-1.8.0 → arraykit-1.10.0}/setup.cfg +0 -0
  19. {arraykit-1.8.0 → arraykit-1.10.0}/setup.py +0 -0
  20. {arraykit-1.8.0 → arraykit-1.10.0}/src/__init__.py +0 -0
  21. {arraykit-1.8.0 → arraykit-1.10.0}/src/_arraykit.c +0 -0
  22. {arraykit-1.8.0 → arraykit-1.10.0}/src/array_go.c +0 -0
  23. {arraykit-1.8.0 → arraykit-1.10.0}/src/array_go.h +0 -0
  24. {arraykit-1.8.0 → arraykit-1.10.0}/src/array_to_tuple.c +0 -0
  25. {arraykit-1.8.0 → arraykit-1.10.0}/src/array_to_tuple.h +0 -0
  26. {arraykit-1.8.0 → arraykit-1.10.0}/src/auto_map.h +0 -0
  27. {arraykit-1.8.0 → arraykit-1.10.0}/src/block_index.c +0 -0
  28. {arraykit-1.8.0 → arraykit-1.10.0}/src/block_index.h +0 -0
  29. {arraykit-1.8.0 → arraykit-1.10.0}/src/delimited_to_arrays.c +0 -0
  30. {arraykit-1.8.0 → arraykit-1.10.0}/src/delimited_to_arrays.h +0 -0
  31. {arraykit-1.8.0 → arraykit-1.10.0}/src/methods.c +0 -0
  32. {arraykit-1.8.0 → arraykit-1.10.0}/src/methods.h +0 -0
  33. {arraykit-1.8.0 → arraykit-1.10.0}/src/py.typed +0 -0
  34. {arraykit-1.8.0 → arraykit-1.10.0}/src/tri_map.h +0 -0
  35. {arraykit-1.8.0 → arraykit-1.10.0}/src/utilities.h +0 -0
  36. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_array_go.py +0 -0
  37. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_astype_array.py +0 -0
  38. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_auto_map_property.py +0 -0
  39. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_block_index.py +0 -0
  40. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_delimited_to_arrays.py +0 -0
  41. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_delimited_to_arrays_integration.py +0 -0
  42. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_delimited_to_arrays_property.py +0 -0
  43. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_factorize.py +0 -0
  44. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_fill_directional.py +0 -0
  45. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_group_ordering.py +0 -0
  46. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_nonzero_1d.py +0 -0
  47. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_nonzero_1d_property.py +0 -0
  48. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_objectable.py +0 -0
  49. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_pyi.py +0 -0
  50. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_split_after_count.py +0 -0
  51. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_transition_slices_from_group.py +0 -0
  52. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_type_discovery.py +0 -0
  53. {arraykit-1.8.0 → arraykit-1.10.0}/test/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arraykit
3
- Version: 1.8.0
3
+ Version: 1.10.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.10.0
68
+ ............
69
+
70
+ Added ``TriMap.register_pairs()``.
71
+
72
+
73
+ 1.9.0
74
+ ............
75
+
76
+ Added ``FrozenAutoMap.get_all_fill()`` and ``TriMap.register_many_from_one()``.
77
+
78
+
67
79
  1.8.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.10.0
39
+ ............
40
+
41
+ Added ``TriMap.register_pairs()``.
42
+
43
+
44
+ 1.9.0
45
+ ............
46
+
47
+ Added ``FrozenAutoMap.get_all_fill()`` and ``TriMap.register_many_from_one()``.
48
+
49
+
38
50
  1.8.0
39
51
  ............
40
52
 
@@ -0,0 +1,2 @@
1
+ 1.10.0
2
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arraykit
3
- Version: 1.8.0
3
+ Version: 1.10.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.10.0
68
+ ............
69
+
70
+ Added ``TriMap.register_pairs()``.
71
+
72
+
73
+ 1.9.0
74
+ ............
75
+
76
+ Added ``FrozenAutoMap.get_all_fill()`` and ``TriMap.register_many_from_one()``.
77
+
78
+
67
79
  1.8.0
68
80
  ............
69
81
 
@@ -53,6 +53,8 @@ class TriMap:
53
53
  def __init__(self, /, src_len: int, dst_len: int) -> None: ...
54
54
  def __repr__(self) -> str: ...
55
55
  def register_one(self, /, src_from: int, dst_from: int) -> None: ...
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: ...
56
58
  def register_unmatched_dst(self) -> None: ...
57
59
  def register_many(self, /, src_from: int, dst_from: np.ndarray) -> None: ...
58
60
  def finalize(self) -> None: ...
@@ -142,6 +144,7 @@ class FrozenAutoMap:
142
144
  def items(self) -> tp.Iterator[tuple[_TLabel, int]]: ...
143
145
  def values(self) -> tp.Iterator[int]: ...
144
146
  def get_all(self, __key: list[_TLabel] | np.ndarray) -> np.ndarray: ...
147
+ def get_all_fill(self, __key: list[_TLabel] | np.ndarray) -> np.ndarray: ...
145
148
  def get_any(self, __key: list[_TLabel] | np.ndarray) -> list[int]: ...
146
149
  def __iter__(self) -> tp.Iterator[_TLabel]: ...
147
150
  def __getitem__(self, __key: tp.Any) -> int: ...
@@ -1919,6 +1919,209 @@ fam_get_all(FAMObject *self, PyObject *key) {
1919
1919
  # undef GET_ALL_FLEXIBLE
1920
1920
 
1921
1921
 
1922
+ // Fill variants of the GET_ALL_* macros: on a miss, write -1 into the output slot and
1923
+ // continue (instead of raising KeyError). Depend on self, key_size, key_array, i, b, array.
1924
+ # define GET_ALL_FILL_SCALARS(npy_type_src, npy_type_dst, kat, lookup_func, hash_func, post_deref) \
1925
+ { \
1926
+ npy_type_dst v; \
1927
+ Py_ssize_t table_pos; \
1928
+ for (; i < key_size; i++) { \
1929
+ v = post_deref(*(npy_type_src*)PyArray_GETPTR1(key_array, i)); \
1930
+ table_pos = lookup_func(self, v, hash_func(v), kat); \
1931
+ if (table_pos < 0 || (self->table[table_pos].hash == -1)) { \
1932
+ if (PyErr_Occurred()) { \
1933
+ Py_DECREF(array); \
1934
+ return NULL; \
1935
+ } \
1936
+ b[i] = -1; \
1937
+ continue; \
1938
+ } \
1939
+ b[i] = (npy_int64)self->table[table_pos].keys_pos; \
1940
+ } \
1941
+ } \
1942
+
1943
+ # define GET_ALL_FILL_DT64(npy_type_src, npy_type_dst, kat, lookup_func, hash_func) \
1944
+ { \
1945
+ npy_type_dst v; \
1946
+ Py_ssize_t table_pos; \
1947
+ for (; i < key_size; i++) { \
1948
+ v = *(npy_type_src*)PyArray_GETPTR1(key_array, i); \
1949
+ table_pos = lookup_func(self, v, hash_func(v), kat); \
1950
+ if (table_pos < 0 || (self->table[table_pos].hash == -1)) { \
1951
+ if (PyErr_Occurred()) { \
1952
+ Py_DECREF(array); \
1953
+ return NULL; \
1954
+ } \
1955
+ b[i] = -1; \
1956
+ continue; \
1957
+ } \
1958
+ b[i] = (npy_int64)self->table[table_pos].keys_pos; \
1959
+ } \
1960
+ } \
1961
+
1962
+ # define GET_ALL_FILL_FLEXIBLE(char_type, get_end_func, lookup_func, hash_func) \
1963
+ { \
1964
+ char_type* v; \
1965
+ Py_ssize_t dt_size = PyArray_ITEMSIZE(key_array) / sizeof(char_type); \
1966
+ Py_ssize_t k_size; \
1967
+ Py_ssize_t table_pos; \
1968
+ for (; i < key_size; i++) { \
1969
+ v = (char_type*)PyArray_GETPTR1(key_array, i); \
1970
+ k_size = get_end_func(v, dt_size) - v; \
1971
+ table_pos = lookup_func(self, v, k_size, hash_func(v, k_size)); \
1972
+ if (table_pos < 0 || (self->table[table_pos].hash == -1)) { \
1973
+ if (PyErr_Occurred()) { \
1974
+ Py_DECREF(array); \
1975
+ return NULL; \
1976
+ } \
1977
+ b[i] = -1; \
1978
+ continue; \
1979
+ } \
1980
+ b[i] = (npy_int64)self->table[table_pos].keys_pos; \
1981
+ } \
1982
+ } \
1983
+
1984
+ // Given a list or array of keys, return an input-aligned int64 array of looked-up
1985
+ // positions, with -1 for any key not found (never raises KeyError). This is the
1986
+ // "vectorized get with default -1" needed by left/outer joins. Immutable output.
1987
+ static PyObject *
1988
+ fam_get_all_fill(FAMObject *self, PyObject *key) {
1989
+ Py_ssize_t key_size = 0;
1990
+ Py_ssize_t keys_pos = -1;
1991
+ PyObject* k = NULL;
1992
+ PyObject *array = NULL;
1993
+ Py_ssize_t i = 0;
1994
+
1995
+ int key_is_list;
1996
+ if (PyList_CheckExact(key)) {
1997
+ key_is_list = 1;
1998
+ key_size = PyList_GET_SIZE(key);
1999
+ }
2000
+ else if (PyArray_Check(key)) {
2001
+ key_is_list = 0;
2002
+ key_size = PyArray_SIZE((PyArrayObject *)key);
2003
+ }
2004
+ else {
2005
+ PyErr_SetString(PyExc_TypeError, "Must provide a list or array.");
2006
+ return NULL;
2007
+ }
2008
+
2009
+ npy_intp dims[] = {key_size};
2010
+ array = PyArray_EMPTY(1, dims, NPY_INT64, 0);
2011
+ if (array == NULL) {
2012
+ return NULL;
2013
+ }
2014
+ npy_int64* b = (npy_int64*)PyArray_DATA((PyArrayObject*)array);
2015
+
2016
+ if (key_is_list) {
2017
+ for (; i < key_size; i++) {
2018
+ k = PyList_GET_ITEM(key, i); // borrow
2019
+ keys_pos = lookup(self, k);
2020
+ if (keys_pos < 0) {
2021
+ if (PyErr_Occurred()) {
2022
+ Py_DECREF(array);
2023
+ return NULL;
2024
+ }
2025
+ b[i] = -1;
2026
+ continue;
2027
+ }
2028
+ b[i] = (npy_int64)keys_pos;
2029
+ }
2030
+ }
2031
+ else { // key is an array
2032
+ PyArrayObject* key_array = (PyArrayObject *)key;
2033
+ int key_array_t = PyArray_TYPE(key_array);
2034
+ int use_typed = kat_is_kind(self->keys_array_type, PyArray_DESCR(key_array)->kind);
2035
+ if (use_typed && key_array_t == NPY_DATETIME) {
2036
+ NPY_DATETIMEUNIT key_unit = AK_dt_unit_from_array(key_array);
2037
+ if (!kat_is_datetime_unit(self->keys_array_type, key_unit)) {
2038
+ // mismatched units: use the coercing scalar path (below) rather than
2039
+ // raising, so equal instants still resolve (and true misses give -1)
2040
+ use_typed = 0;
2041
+ }
2042
+ }
2043
+ if (use_typed) {
2044
+ switch (key_array_t) {
2045
+ case NPY_INT64:
2046
+ GET_ALL_FILL_SCALARS(npy_int64, npy_int64, KAT_INT64, lookup_hash_int, int_to_hash,);
2047
+ break;
2048
+ case NPY_INT32:
2049
+ GET_ALL_FILL_SCALARS(npy_int32, npy_int64, KAT_INT32, lookup_hash_int, int_to_hash,);
2050
+ break;
2051
+ case NPY_INT16:
2052
+ GET_ALL_FILL_SCALARS(npy_int16, npy_int64, KAT_INT16, lookup_hash_int, int_to_hash,);
2053
+ break;
2054
+ case NPY_INT8:
2055
+ GET_ALL_FILL_SCALARS(npy_int8, npy_int64, KAT_INT8, lookup_hash_int, int_to_hash,);
2056
+ break;
2057
+ case NPY_UINT64:
2058
+ GET_ALL_FILL_SCALARS(npy_uint64, npy_uint64, KAT_UINT64, lookup_hash_uint, uint_to_hash,);
2059
+ break;
2060
+ case NPY_UINT32:
2061
+ GET_ALL_FILL_SCALARS(npy_uint32, npy_uint64, KAT_UINT32, lookup_hash_uint, uint_to_hash,);
2062
+ break;
2063
+ case NPY_UINT16:
2064
+ GET_ALL_FILL_SCALARS(npy_uint16, npy_uint64, KAT_UINT16, lookup_hash_uint, uint_to_hash,);
2065
+ break;
2066
+ case NPY_UINT8:
2067
+ GET_ALL_FILL_SCALARS(npy_uint8, npy_uint64, KAT_UINT8, lookup_hash_uint, uint_to_hash,);
2068
+ break;
2069
+ case NPY_FLOAT64:
2070
+ GET_ALL_FILL_SCALARS(npy_double, npy_double, KAT_FLOAT64, lookup_hash_double, double_to_hash,);
2071
+ break;
2072
+ case NPY_FLOAT32:
2073
+ GET_ALL_FILL_SCALARS(npy_float, npy_double, KAT_FLOAT32, lookup_hash_double, double_to_hash,);
2074
+ break;
2075
+ case NPY_FLOAT16:
2076
+ GET_ALL_FILL_SCALARS(npy_half, npy_double, KAT_FLOAT16, lookup_hash_double, double_to_hash, npy_half_to_double);
2077
+ break;
2078
+ case NPY_UNICODE:
2079
+ GET_ALL_FILL_FLEXIBLE(Py_UCS4, ucs4_get_end_p, lookup_hash_unicode, unicode_to_hash);
2080
+ break;
2081
+ case NPY_STRING:
2082
+ GET_ALL_FILL_FLEXIBLE(char, char_get_end_p, lookup_hash_string, string_to_hash);
2083
+ break;
2084
+ case NPY_DATETIME:
2085
+ GET_ALL_FILL_DT64(npy_int64, npy_int64, KAT_INT64, lookup_hash_int, int_to_hash);
2086
+ break;
2087
+ default:
2088
+ use_typed = 0; // unhandled kind: fall to the scalar path below
2089
+ break;
2090
+ }
2091
+ }
2092
+ if (!use_typed) {
2093
+ for (; i < key_size; i++) {
2094
+ k = PyArray_ToScalar(PyArray_GETPTR1(key_array, i), key_array);
2095
+ if (k == NULL) {
2096
+ Py_DECREF(array);
2097
+ return NULL;
2098
+ }
2099
+ keys_pos = lookup(self, k);
2100
+ if (keys_pos < 0) {
2101
+ if (PyErr_Occurred()) {
2102
+ Py_DECREF(k);
2103
+ Py_DECREF(array);
2104
+ return NULL;
2105
+ }
2106
+ Py_DECREF(k);
2107
+ b[i] = -1;
2108
+ continue;
2109
+ }
2110
+ Py_DECREF(k);
2111
+ b[i] = (npy_int64)keys_pos;
2112
+ }
2113
+ }
2114
+ }
2115
+
2116
+ PyArray_CLEARFLAGS((PyArrayObject *)array, NPY_ARRAY_WRITEABLE);
2117
+ return array;
2118
+ }
2119
+
2120
+ # undef GET_ALL_FILL_SCALARS
2121
+ # undef GET_ALL_FILL_DT64
2122
+ # undef GET_ALL_FILL_FLEXIBLE
2123
+
2124
+
1922
2125
  static inline int
1923
2126
  append_ssize_t(
1924
2127
  PyObject* list,
@@ -2974,6 +3177,7 @@ static PyMethodDef fam_methods[] = {
2974
3177
  {"keys", (PyCFunction) fam_keys, METH_NOARGS, NULL},
2975
3178
  {"values", (PyCFunction) fam_values, METH_NOARGS, NULL},
2976
3179
  {"get_all", (PyCFunction) fam_get_all, METH_O, NULL},
3180
+ {"get_all_fill", (PyCFunction) fam_get_all_fill, METH_O, NULL},
2977
3181
  {"get_any", (PyCFunction) fam_get_any, METH_O, NULL},
2978
3182
  {NULL},
2979
3183
  };
@@ -269,6 +269,101 @@ TriMap_register_one(TriMapObject *self, PyObject *args) {
269
269
  Py_RETURN_NONE;
270
270
  }
271
271
 
272
+ // Bulk one-to-one registration: given an int64 array `dst_pos` of length `src_len`,
273
+ // register src position i to dst position dst_pos[i] (or -1 for an unmatched src) in a
274
+ // single C loop -- equivalent to calling register_one(i, dst_pos[i]) for each i, but
275
+ // without per-element Python overhead. Used by the hash-join fast path.
276
+ PyObject *
277
+ TriMap_register_many_from_one(TriMapObject *self, PyObject *arg) {
278
+ if (self->finalized) {
279
+ PyErr_SetString(PyExc_RuntimeError, "Cannot register post finalization");
280
+ return NULL;
281
+ }
282
+ if (!PyArray_Check(arg)) {
283
+ PyErr_SetString(PyExc_TypeError, "Must provide an array");
284
+ return NULL;
285
+ }
286
+ PyArrayObject* a = (PyArrayObject*)arg;
287
+ if (PyArray_TYPE(a) != NPY_INT64) {
288
+ PyErr_SetString(PyExc_ValueError, "Array must be of type int64");
289
+ return NULL;
290
+ }
291
+ if (PyArray_NDIM(a) != 1) {
292
+ PyErr_SetString(PyExc_ValueError, "Array must be 1-dimensional");
293
+ return NULL;
294
+ }
295
+ if (!PyArray_IS_C_CONTIGUOUS(a)) {
296
+ PyErr_SetString(PyExc_ValueError, "Array must be contiguous");
297
+ return NULL;
298
+ }
299
+ npy_intp n = PyArray_SIZE(a);
300
+ if (n != self->src_len) {
301
+ PyErr_SetString(PyExc_ValueError, "Array length must equal src_len");
302
+ return NULL;
303
+ }
304
+ const npy_int64* d = (npy_int64*)PyArray_DATA(a);
305
+ for (npy_intp i = 0; i < n; i++) {
306
+ if (AK_TM_register_one(self, (Py_ssize_t)i, (Py_ssize_t)d[i])) {
307
+ return NULL;
308
+ }
309
+ }
310
+ Py_RETURN_NONE;
311
+ }
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
+
272
367
  PyObject *
273
368
  TriMap_register_unmatched_dst(TriMapObject *self) {
274
369
  if (self->finalized) {
@@ -1358,6 +1453,8 @@ TriMap_map_dst_fill(TriMapObject *self, PyObject *args) {
1358
1453
 
1359
1454
  static PyMethodDef TriMap_methods[] = {
1360
1455
  {"register_one", (PyCFunction)TriMap_register_one, METH_VARARGS, NULL},
1456
+ {"register_many_from_one", (PyCFunction)TriMap_register_many_from_one, METH_O, NULL},
1457
+ {"register_pairs", (PyCFunction)TriMap_register_pairs, METH_VARARGS, NULL},
1361
1458
  {"register_unmatched_dst", (PyCFunction)TriMap_register_unmatched_dst, METH_NOARGS, NULL},
1362
1459
  {"register_many", (PyCFunction)TriMap_register_many, METH_VARARGS, NULL},
1363
1460
  {"finalize", (PyCFunction)TriMap_finalize, METH_NOARGS, NULL},
@@ -916,6 +916,109 @@ def test_fam_array_get_all_m3():
916
916
  assert post2.tolist() == [2, 1]
917
917
 
918
918
 
919
+ # -------------------------------------------------------------------------------
920
+ # get_all_fill: input-aligned lookup, -1 for misses (never raises KeyError)
921
+
922
+
923
+ def test_fam_array_get_all_fill_int_a():
924
+ a1 = np.array((10, 20, 30, 40), dtype=np.int64)
925
+ a1.flags.writeable = False
926
+ fam = FrozenAutoMap(a1)
927
+ # all present -> identical to get_all
928
+ keys = np.array([30, 10, 40], dtype=np.int64)
929
+ keys.flags.writeable = False
930
+ assert fam.get_all_fill(keys).tolist() == [2, 0, 3]
931
+ assert fam.get_all_fill(keys).tolist() == fam.get_all(keys).tolist()
932
+
933
+
934
+ def test_fam_array_get_all_fill_int_missing():
935
+ a1 = np.array((10, 20, 30, 40), dtype=np.int64)
936
+ a1.flags.writeable = False
937
+ fam = FrozenAutoMap(a1)
938
+ keys = np.array([30, 99, 10, 77], dtype=np.int64)
939
+ keys.flags.writeable = False
940
+ assert fam.get_all_fill(keys).tolist() == [2, -1, 0, -1]
941
+
942
+
943
+ def test_fam_array_get_all_fill_immutable_and_dtype():
944
+ a1 = np.array((10, 20, 30), dtype=np.int64)
945
+ a1.flags.writeable = False
946
+ fam = FrozenAutoMap(a1)
947
+ keys = np.array([10], dtype=np.int64)
948
+ keys.flags.writeable = False
949
+ post = fam.get_all_fill(keys)
950
+ assert not post.flags.writeable
951
+ assert post.dtype == np.dtype(np.int64)
952
+
953
+
954
+ def test_fam_array_get_all_fill_empty():
955
+ a1 = np.array((10, 20), dtype=np.int64)
956
+ a1.flags.writeable = False
957
+ fam = FrozenAutoMap(a1)
958
+ keys = np.array([], dtype=np.int64)
959
+ keys.flags.writeable = False
960
+ assert fam.get_all_fill(keys).tolist() == []
961
+
962
+
963
+ def test_fam_array_get_all_fill_all_missing():
964
+ a1 = np.array((10, 20), dtype=np.int64)
965
+ a1.flags.writeable = False
966
+ fam = FrozenAutoMap(a1)
967
+ keys = np.array([1, 2, 3], dtype=np.int64)
968
+ keys.flags.writeable = False
969
+ assert fam.get_all_fill(keys).tolist() == [-1, -1, -1]
970
+
971
+
972
+ def test_fam_array_get_all_fill_kind_mismatch():
973
+ # int map queried with a float array -> scalar fallback, -1 for misses
974
+ a1 = np.array((10, 20, 30), dtype=np.int64)
975
+ a1.flags.writeable = False
976
+ fam = FrozenAutoMap(a1)
977
+ keys = np.array([10.0, 15.0, 30.0], dtype=np.float64)
978
+ keys.flags.writeable = False
979
+ assert fam.get_all_fill(keys).tolist() == [0, -1, 2]
980
+
981
+
982
+ def test_fam_array_get_all_fill_unicode():
983
+ a1 = np.array(['a', 'bb', 'c'])
984
+ a1.flags.writeable = False
985
+ fam = FrozenAutoMap(a1)
986
+ keys = np.array(['c', 'zz', 'a'])
987
+ keys.flags.writeable = False
988
+ assert fam.get_all_fill(keys).tolist() == [2, -1, 0]
989
+
990
+
991
+ def test_fam_array_get_all_fill_bytes():
992
+ a1 = np.array([b'a', b'bb', b'c'], dtype='S2')
993
+ a1.flags.writeable = False
994
+ fam = FrozenAutoMap(a1)
995
+ keys = np.array([b'c', b'zz', b'a'], dtype='S2')
996
+ keys.flags.writeable = False
997
+ assert fam.get_all_fill(keys).tolist() == [2, -1, 0]
998
+
999
+
1000
+ def test_fam_array_get_all_fill_datetime():
1001
+ a1 = np.array(['2020-01', '2021-06', '2022-03'], dtype='datetime64[M]')
1002
+ a1.flags.writeable = False
1003
+ fam = FrozenAutoMap(a1)
1004
+ keys = np.array(['2022-03', '1999-12', '2020-01'], dtype='datetime64[M]')
1005
+ keys.flags.writeable = False
1006
+ assert fam.get_all_fill(keys).tolist() == [2, -1, 0]
1007
+
1008
+
1009
+ def test_fam_array_get_all_fill_object_list():
1010
+ fam = FrozenAutoMap([('x',), ('y',)])
1011
+ assert fam.get_all_fill([('y',), ('z',), ('x',)]).tolist() == [1, -1, 0]
1012
+
1013
+
1014
+ def test_fam_array_get_all_fill_bad_type():
1015
+ a1 = np.array((10, 20), dtype=np.int64)
1016
+ a1.flags.writeable = False
1017
+ fam = FrozenAutoMap(a1)
1018
+ with pytest.raises(TypeError):
1019
+ fam.get_all_fill('a')
1020
+
1021
+
919
1022
  # -------------------------------------------------------------------------------
920
1023
 
921
1024
 
@@ -1439,3 +1439,226 @@ class TestUnit(unittest.TestCase):
1439
1439
  np.datetime64('2005-11'),
1440
1440
  ],
1441
1441
  )
1442
+
1443
+ # ------------------------------------------------------------------
1444
+ # register_many_from_one (bulk one-to-one)
1445
+
1446
+ def test_tri_map_register_many_from_one_a(self) -> None:
1447
+ tm = TriMap(3, 4)
1448
+ tm.register_many_from_one(np.array([2, 0, 3], dtype=np.int64))
1449
+ tm.finalize()
1450
+ self.assertFalse(tm.is_many())
1451
+ src = np.array([10, 20, 30])
1452
+ dst = np.array([100, 200, 300, 400])
1453
+ self.assertEqual(tm.map_src_no_fill(src).tolist(), [10, 20, 30])
1454
+ self.assertEqual(tm.map_dst_no_fill(dst).tolist(), [300, 100, 400])
1455
+
1456
+ def test_tri_map_register_many_from_one_unmatched(self) -> None:
1457
+ # -1 marks an unmatched src -> a src-only row (dst side gets a fill there)
1458
+ tm = TriMap(3, 4)
1459
+ tm.register_many_from_one(np.array([2, -1, 0], dtype=np.int64))
1460
+ tm.finalize()
1461
+ self.assertTrue(tm.src_no_fill()) # every output row has a src value
1462
+ self.assertFalse(tm.dst_no_fill()) # row 1 has no dst -> needs fill
1463
+ dst = np.array([100, 200, 300, 400])
1464
+ self.assertEqual(tm.map_dst_fill(dst, -9, np.dtype(np.int64)).tolist(), [300, -9, 100])
1465
+
1466
+ def test_tri_map_register_many_from_one_is_many(self) -> None:
1467
+ # two src rows matching the same dst -> is_many
1468
+ tm = TriMap(3, 3)
1469
+ tm.register_many_from_one(np.array([0, 0, 1], dtype=np.int64))
1470
+ tm.finalize()
1471
+ self.assertTrue(tm.is_many())
1472
+
1473
+ def test_tri_map_register_many_from_one_equivalence(self) -> None:
1474
+ rng = np.random.RandomState(0)
1475
+ for _ in range(50):
1476
+ src_len = int(rng.randint(1, 12))
1477
+ dst_len = int(rng.randint(1, 10))
1478
+ dst_pos = rng.randint(-1, dst_len, size=src_len).astype(np.int64)
1479
+ tb = TriMap(src_len, dst_len)
1480
+ tb.register_many_from_one(dst_pos)
1481
+ tb.finalize()
1482
+ tl = TriMap(src_len, dst_len)
1483
+ for i in range(src_len):
1484
+ tl.register_one(i, int(dst_pos[i]))
1485
+ tl.finalize()
1486
+ src = np.arange(100, 100 + src_len)
1487
+ dst = np.arange(200, 200 + dst_len)
1488
+ self.assertEqual(tb.is_many(), tl.is_many())
1489
+ self.assertEqual(
1490
+ tb.map_src_no_fill(src).tolist(), tl.map_src_no_fill(src).tolist()
1491
+ )
1492
+ self.assertEqual(
1493
+ tb.map_dst_fill(dst, -1, np.dtype(np.int64)).tolist(),
1494
+ tl.map_dst_fill(dst, -1, np.dtype(np.int64)).tolist(),
1495
+ )
1496
+
1497
+ def test_tri_map_register_many_from_one_errors(self) -> None:
1498
+ with self.assertRaises(ValueError): # wrong length
1499
+ TriMap(3, 3).register_many_from_one(np.array([0, 1], dtype=np.int64))
1500
+ with self.assertRaises(ValueError): # wrong dtype
1501
+ TriMap(3, 3).register_many_from_one(np.array([0, 1, 2], dtype=np.int32))
1502
+ with self.assertRaises(ValueError): # out of bounds dst
1503
+ TriMap(3, 3).register_many_from_one(np.array([0, 1, 9], dtype=np.int64))
1504
+ with self.assertRaises(TypeError): # not an array
1505
+ TriMap(3, 3).register_many_from_one([0, 1, 2])
1506
+ tm = TriMap(2, 2)
1507
+ tm.register_many_from_one(np.array([0, 1], dtype=np.int64))
1508
+ tm.finalize()
1509
+ with self.assertRaises(RuntimeError): # post-finalize
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.8.0/VERSION DELETED
@@ -1,2 +0,0 @@
1
- 1.8.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
File without changes
File without changes
File without changes