arraykit 1.7.0__tar.gz → 1.9.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.7.0/arraykit.egg-info → arraykit-1.9.0}/PKG-INFO +13 -1
  2. {arraykit-1.7.0 → arraykit-1.9.0}/README.rst +12 -0
  3. arraykit-1.9.0/VERSION +2 -0
  4. {arraykit-1.7.0 → arraykit-1.9.0/arraykit.egg-info}/PKG-INFO +13 -1
  5. {arraykit-1.7.0 → arraykit-1.9.0}/arraykit.egg-info/SOURCES.txt +1 -0
  6. {arraykit-1.7.0 → arraykit-1.9.0}/src/__init__.py +1 -0
  7. {arraykit-1.7.0 → arraykit-1.9.0}/src/__init__.pyi +10 -0
  8. {arraykit-1.7.0 → arraykit-1.9.0}/src/_arraykit.c +4 -0
  9. {arraykit-1.7.0 → arraykit-1.9.0}/src/auto_map.c +204 -0
  10. {arraykit-1.7.0 → arraykit-1.9.0}/src/methods.c +157 -0
  11. {arraykit-1.7.0 → arraykit-1.9.0}/src/methods.h +3 -0
  12. {arraykit-1.7.0 → arraykit-1.9.0}/src/tri_map.c +42 -0
  13. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_auto_map.py +103 -0
  14. arraykit-1.9.0/test/test_fill_directional.py +229 -0
  15. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_tri_map.py +69 -0
  16. arraykit-1.7.0/VERSION +0 -2
  17. {arraykit-1.7.0 → arraykit-1.9.0}/LICENSE.txt +0 -0
  18. {arraykit-1.7.0 → arraykit-1.9.0}/MANIFEST.in +0 -0
  19. {arraykit-1.7.0 → arraykit-1.9.0}/arraykit.egg-info/dependency_links.txt +0 -0
  20. {arraykit-1.7.0 → arraykit-1.9.0}/arraykit.egg-info/requires.txt +0 -0
  21. {arraykit-1.7.0 → arraykit-1.9.0}/arraykit.egg-info/top_level.txt +0 -0
  22. {arraykit-1.7.0 → arraykit-1.9.0}/pyproject.toml +0 -0
  23. {arraykit-1.7.0 → arraykit-1.9.0}/setup.cfg +0 -0
  24. {arraykit-1.7.0 → arraykit-1.9.0}/setup.py +0 -0
  25. {arraykit-1.7.0 → arraykit-1.9.0}/src/array_go.c +0 -0
  26. {arraykit-1.7.0 → arraykit-1.9.0}/src/array_go.h +0 -0
  27. {arraykit-1.7.0 → arraykit-1.9.0}/src/array_to_tuple.c +0 -0
  28. {arraykit-1.7.0 → arraykit-1.9.0}/src/array_to_tuple.h +0 -0
  29. {arraykit-1.7.0 → arraykit-1.9.0}/src/auto_map.h +0 -0
  30. {arraykit-1.7.0 → arraykit-1.9.0}/src/block_index.c +0 -0
  31. {arraykit-1.7.0 → arraykit-1.9.0}/src/block_index.h +0 -0
  32. {arraykit-1.7.0 → arraykit-1.9.0}/src/delimited_to_arrays.c +0 -0
  33. {arraykit-1.7.0 → arraykit-1.9.0}/src/delimited_to_arrays.h +0 -0
  34. {arraykit-1.7.0 → arraykit-1.9.0}/src/py.typed +0 -0
  35. {arraykit-1.7.0 → arraykit-1.9.0}/src/tri_map.h +0 -0
  36. {arraykit-1.7.0 → arraykit-1.9.0}/src/utilities.h +0 -0
  37. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_array_go.py +0 -0
  38. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_astype_array.py +0 -0
  39. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_auto_map_property.py +0 -0
  40. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_block_index.py +0 -0
  41. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_delimited_to_arrays.py +0 -0
  42. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_delimited_to_arrays_integration.py +0 -0
  43. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_delimited_to_arrays_property.py +0 -0
  44. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_factorize.py +0 -0
  45. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_group_ordering.py +0 -0
  46. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_nonzero_1d.py +0 -0
  47. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_nonzero_1d_property.py +0 -0
  48. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_objectable.py +0 -0
  49. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_pyi.py +0 -0
  50. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_split_after_count.py +0 -0
  51. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_transition_slices_from_group.py +0 -0
  52. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_type_discovery.py +0 -0
  53. {arraykit-1.7.0 → arraykit-1.9.0}/test/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arraykit
3
- Version: 1.7.0
3
+ Version: 1.9.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.9.0
68
+ ............
69
+
70
+ Added ``FrozenAutoMap.get_all_fill()`` and ``TriMap.register_many_from_one()``.
71
+
72
+
73
+ 1.8.0
74
+ ............
75
+
76
+ Added ``fill_directional()``.
77
+
78
+
67
79
  1.7.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.9.0
39
+ ............
40
+
41
+ Added ``FrozenAutoMap.get_all_fill()`` and ``TriMap.register_many_from_one()``.
42
+
43
+
44
+ 1.8.0
45
+ ............
46
+
47
+ Added ``fill_directional()``.
48
+
49
+
38
50
  1.7.0
39
51
  ............
40
52
 
arraykit-1.9.0/VERSION ADDED
@@ -0,0 +1,2 @@
1
+ 1.9.0
2
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arraykit
3
- Version: 1.7.0
3
+ Version: 1.9.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.9.0
68
+ ............
69
+
70
+ Added ``FrozenAutoMap.get_all_fill()`` and ``TriMap.register_many_from_one()``.
71
+
72
+
73
+ 1.8.0
74
+ ............
75
+
76
+ Added ``fill_directional()``.
77
+
78
+
67
79
  1.7.0
68
80
  ............
69
81
 
@@ -37,6 +37,7 @@ test/test_delimited_to_arrays.py
37
37
  test/test_delimited_to_arrays_integration.py
38
38
  test/test_delimited_to_arrays_property.py
39
39
  test/test_factorize.py
40
+ test/test_fill_directional.py
40
41
  test/test_group_ordering.py
41
42
  test/test_nonzero_1d.py
42
43
  test/test_nonzero_1d_property.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 fill_directional as fill_directional
30
31
  from ._arraykit import count_iteration as count_iteration
31
32
  from ._arraykit import first_true_1d as first_true_1d
32
33
  from ._arraykit import first_true_2d as first_true_2d
@@ -53,6 +53,7 @@ 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: ...
56
57
  def register_unmatched_dst(self) -> None: ...
57
58
  def register_many(self, /, src_from: int, dst_from: np.ndarray) -> None: ...
58
59
  def finalize(self) -> None: ...
@@ -142,6 +143,7 @@ class FrozenAutoMap:
142
143
  def items(self) -> tp.Iterator[tuple[_TLabel, int]]: ...
143
144
  def values(self) -> tp.Iterator[int]: ...
144
145
  def get_all(self, __key: list[_TLabel] | np.ndarray) -> np.ndarray: ...
146
+ def get_all_fill(self, __key: list[_TLabel] | np.ndarray) -> np.ndarray: ...
145
147
  def get_any(self, __key: list[_TLabel] | np.ndarray) -> list[int]: ...
146
148
  def __iter__(self) -> tp.Iterator[_TLabel]: ...
147
149
  def __getitem__(self, __key: tp.Any) -> int: ...
@@ -230,6 +232,14 @@ def factorize(
230
232
  def group_ordering(
231
233
  codes: np.ndarray, *, size: tp.Optional[int] = ...
232
234
  ) -> tp.Tuple[np.ndarray, np.ndarray]: ...
235
+ def fill_directional(
236
+ array: np.ndarray,
237
+ target: np.ndarray,
238
+ *,
239
+ forward: bool = ...,
240
+ axis: int = ...,
241
+ limit: int = ...,
242
+ ) -> np.ndarray: ...
233
243
  def first_true_1d(__array: np.ndarray, *, forward: bool) -> int: ...
234
244
  def first_true_2d(__array: np.ndarray, *, forward: bool, axis: int) -> np.ndarray: ...
235
245
  def nonzero_1d(__array: np.ndarray, /) -> 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
+ {"fill_directional",
82
+ (PyCFunction)fill_directional,
83
+ METH_VARARGS | METH_KEYWORDS,
84
+ NULL},
81
85
  {NULL},
82
86
  };
83
87
 
@@ -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
  };
@@ -1128,6 +1128,163 @@ fail:
1128
1128
  return NULL;
1129
1129
  }
1130
1130
 
1131
+ // Fill one strided lane in place: walk positions in the fill direction, carrying
1132
+ // the most recent non-target value into each target position (subject to `limit`
1133
+ // consecutive fills per run). `elem_base`/`elem_stride` address elements in bytes;
1134
+ // `target_base`/`target_stride` address the aligned Boolean target lane. Object arrays
1135
+ // duplicate the carried PyObject* with balanced refcounts; all other dtypes memcpy
1136
+ // the itemsize bytes. Leading targets (no prior source) are left unchanged.
1137
+ static inline void
1138
+ AK_fill_lane(
1139
+ char *elem_base,
1140
+ npy_intp elem_stride,
1141
+ const npy_bool *target_base,
1142
+ npy_intp target_stride,
1143
+ npy_intp length,
1144
+ npy_intp itemsize,
1145
+ int is_object,
1146
+ int forward,
1147
+ npy_intp limit)
1148
+ {
1149
+ char *last_valid = NULL;
1150
+ npy_intp count = 0;
1151
+ for (npy_intp k = 0; k < length; k++) {
1152
+ npy_intp pos = forward ? k : (length - 1 - k);
1153
+ char *elem = elem_base + pos * elem_stride;
1154
+ if (target_base[pos * target_stride]) {
1155
+ if (last_valid != NULL && (limit == 0 || count < limit)) {
1156
+ if (is_object) {
1157
+ PyObject **dst = (PyObject**)elem;
1158
+ PyObject **src = (PyObject**)last_valid;
1159
+ Py_INCREF(*src);
1160
+ Py_XDECREF(*dst);
1161
+ *dst = *src;
1162
+ }
1163
+ else {
1164
+ memcpy(elem, last_valid, (size_t)itemsize);
1165
+ }
1166
+ count++;
1167
+ }
1168
+ // else: leading target (no source yet) or limit reached; leave as-is
1169
+ }
1170
+ else {
1171
+ last_valid = elem;
1172
+ count = 0;
1173
+ }
1174
+ }
1175
+ }
1176
+
1177
+ static char *fill_directional_kwarg_names[] = {
1178
+ "array",
1179
+ "target",
1180
+ "forward",
1181
+ "axis",
1182
+ "limit",
1183
+ NULL
1184
+ };
1185
+
1186
+ // Directional (forward or backward) fill of `array` along `axis`, replacing each
1187
+ // position flagged True in the Boolean `target` mask with the last (forward) or
1188
+ // next (backward) non-target value. A single O(n) pass per lane; `limit` caps the
1189
+ // number of consecutive fills per run (0 == unlimited). The target mask is supplied
1190
+ // by the caller (e.g. isna/isfalsy), keeping this dtype- and predicate-agnostic.
1191
+ // Returns a new, immutable, C-contiguous array.
1192
+ PyObject *
1193
+ fill_directional(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs)
1194
+ {
1195
+ PyArrayObject *array = NULL;
1196
+ PyArrayObject *target = NULL;
1197
+ int forward = 1;
1198
+ int axis = 0;
1199
+ Py_ssize_t limit = 0;
1200
+
1201
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs,
1202
+ "O!O!|$pin:fill_directional",
1203
+ fill_directional_kwarg_names,
1204
+ &PyArray_Type, &array,
1205
+ &PyArray_Type, &target,
1206
+ &forward,
1207
+ &axis,
1208
+ &limit
1209
+ )) {
1210
+ return NULL;
1211
+ }
1212
+
1213
+ int ndim = PyArray_NDIM(array);
1214
+ if (ndim != 1 && ndim != 2) {
1215
+ PyErr_SetString(PyExc_ValueError, "array must be 1- or 2-dimensional");
1216
+ return NULL;
1217
+ }
1218
+ if (PyArray_TYPE(target) != NPY_BOOL) {
1219
+ PyErr_SetString(PyExc_ValueError, "target must be a Boolean array");
1220
+ return NULL;
1221
+ }
1222
+ if (PyArray_NDIM(target) != ndim) {
1223
+ PyErr_SetString(PyExc_ValueError,
1224
+ "target must match the dimensionality of array");
1225
+ return NULL;
1226
+ }
1227
+ for (int i = 0; i < ndim; i++) {
1228
+ if (PyArray_DIM(target, i) != PyArray_DIM(array, i)) {
1229
+ PyErr_SetString(PyExc_ValueError,
1230
+ "target must match the shape of array");
1231
+ return NULL;
1232
+ }
1233
+ }
1234
+ if (!PyArray_IS_C_CONTIGUOUS(target)) {
1235
+ PyErr_SetString(PyExc_ValueError, "target must be C-contiguous");
1236
+ return NULL;
1237
+ }
1238
+ if (ndim == 2 && axis != 0 && axis != 1) {
1239
+ PyErr_SetString(PyExc_ValueError, "axis must be 0 or 1");
1240
+ return NULL;
1241
+ }
1242
+ if (limit < 0) {
1243
+ PyErr_SetString(PyExc_ValueError, "limit must be non-negative");
1244
+ return NULL;
1245
+ }
1246
+
1247
+ // A C-order copy is the result: non-target positions are already correct, and
1248
+ // its layout aligns element-for-element with the C-contiguous target mask.
1249
+ PyObject *out = PyArray_NewCopy(array, NPY_CORDER);
1250
+ if (!out) {
1251
+ return NULL;
1252
+ }
1253
+
1254
+ int is_object = PyArray_TYPE(array) == NPY_OBJECT;
1255
+ npy_intp itemsize = PyArray_ITEMSIZE((PyArrayObject*)out);
1256
+
1257
+ char *out_data = (char*)PyArray_DATA((PyArrayObject*)out);
1258
+ const npy_bool *target_data = (const npy_bool*)PyArray_DATA(target);
1259
+
1260
+ if (ndim == 1) {
1261
+ npy_intp n = PyArray_DIM((PyArrayObject*)out, 0);
1262
+ AK_fill_lane(out_data, itemsize, target_data, 1, n,
1263
+ itemsize, is_object, forward, limit);
1264
+ }
1265
+ else {
1266
+ npy_intp rows = PyArray_DIM((PyArrayObject*)out, 0);
1267
+ npy_intp cols = PyArray_DIM((PyArrayObject*)out, 1);
1268
+ if (axis == 0) {
1269
+ // fill down each column: elements stride by a full row (cols * itemsize)
1270
+ for (npy_intp c = 0; c < cols; c++) {
1271
+ AK_fill_lane(out_data + c * itemsize, cols * itemsize,
1272
+ target_data + c, cols, rows, itemsize, is_object, forward, limit);
1273
+ }
1274
+ }
1275
+ else {
1276
+ // fill across each row: elements are contiguous
1277
+ for (npy_intp r = 0; r < rows; r++) {
1278
+ AK_fill_lane(out_data + r * cols * itemsize, itemsize,
1279
+ target_data + r * cols, 1, cols, itemsize, is_object, forward, limit);
1280
+ }
1281
+ }
1282
+ }
1283
+
1284
+ PyArray_CLEARFLAGS((PyArrayObject*)out, NPY_ARRAY_WRITEABLE);
1285
+ return out;
1286
+ }
1287
+
1131
1288
  PyObject *
1132
1289
  dtype_from_element(PyObject *Py_UNUSED(m), PyObject *arg)
1133
1290
  {
@@ -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
+ fill_directional(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs);
77
+
75
78
  PyObject *
76
79
  dtype_from_element(PyObject *Py_UNUSED(m), PyObject *arg);
77
80
 
@@ -269,6 +269,47 @@ 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
+
272
313
  PyObject *
273
314
  TriMap_register_unmatched_dst(TriMapObject *self) {
274
315
  if (self->finalized) {
@@ -1358,6 +1399,7 @@ TriMap_map_dst_fill(TriMapObject *self, PyObject *args) {
1358
1399
 
1359
1400
  static PyMethodDef TriMap_methods[] = {
1360
1401
  {"register_one", (PyCFunction)TriMap_register_one, METH_VARARGS, NULL},
1402
+ {"register_many_from_one", (PyCFunction)TriMap_register_many_from_one, METH_O, NULL},
1361
1403
  {"register_unmatched_dst", (PyCFunction)TriMap_register_unmatched_dst, METH_NOARGS, NULL},
1362
1404
  {"register_many", (PyCFunction)TriMap_register_many, METH_VARARGS, NULL},
1363
1405
  {"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
 
@@ -0,0 +1,229 @@
1
+ import sys
2
+ import unittest
3
+
4
+ import numpy as np
5
+ from arraykit import fill_directional
6
+
7
+
8
+ def reference(array, target, *, forward=True, axis=0, limit=0):
9
+ """Pure-Python reference: carry the last (forward) or next (backward) non-target
10
+ value across target runs, capped at `limit` consecutive fills."""
11
+ out = array.copy()
12
+
13
+ def fill_lane(vals, tgt):
14
+ last = None
15
+ count = 0
16
+ order = range(len(vals)) if forward else range(len(vals) - 1, -1, -1)
17
+ for i in order:
18
+ if tgt[i]:
19
+ if last is not None and (limit == 0 or count < limit):
20
+ vals[i] = last
21
+ count += 1
22
+ else:
23
+ last = vals[i]
24
+ count = 0
25
+
26
+ if array.ndim == 1:
27
+ fill_lane(out, target)
28
+ elif axis == 0:
29
+ for c in range(out.shape[1]):
30
+ col = out[:, c].copy()
31
+ fill_lane(col, target[:, c])
32
+ out[:, c] = col
33
+ else:
34
+ for r in range(out.shape[0]):
35
+ row = out[r, :].copy()
36
+ fill_lane(row, target[r, :])
37
+ out[r, :] = row
38
+ return out
39
+
40
+
41
+ class TestUnit(unittest.TestCase):
42
+ # ------------------------------------------------------------------
43
+ # basic 1D behavior
44
+
45
+ def test_forward_1d(self) -> None:
46
+ a = np.array([1.0, np.nan, np.nan, 4.0, np.nan])
47
+ t = np.isnan(a)
48
+ post = fill_directional(a, t)
49
+ self.assertEqual(post.tolist(), [1.0, 1.0, 1.0, 4.0, 4.0])
50
+
51
+ def test_backward_1d(self) -> None:
52
+ a = np.array([1.0, np.nan, np.nan, 4.0, np.nan])
53
+ t = np.isnan(a)
54
+ post = fill_directional(a, t, forward=False)
55
+ # trailing target has no source and stays NaN
56
+ self.assertTrue(
57
+ np.array_equal(post, [1.0, 4.0, 4.0, 4.0, np.nan], equal_nan=True)
58
+ )
59
+
60
+ def test_leading_target_unchanged(self) -> None:
61
+ a = np.array([np.nan, np.nan, 3.0, np.nan])
62
+ post = fill_directional(a, np.isnan(a))
63
+ self.assertTrue(
64
+ np.array_equal(post, [np.nan, np.nan, 3.0, 3.0], equal_nan=True)
65
+ )
66
+
67
+ def test_all_target(self) -> None:
68
+ a = np.array([np.nan, np.nan, np.nan])
69
+ post = fill_directional(a, np.isnan(a))
70
+ self.assertTrue(np.array_equal(post, a, equal_nan=True))
71
+
72
+ def test_no_target(self) -> None:
73
+ a = np.arange(5.0)
74
+ post = fill_directional(a, np.zeros(5, dtype=bool))
75
+ self.assertEqual(post.tolist(), a.tolist())
76
+
77
+ def test_empty(self) -> None:
78
+ a = np.array([], dtype=float)
79
+ post = fill_directional(a, np.array([], dtype=bool))
80
+ self.assertEqual(len(post), 0)
81
+
82
+ # ------------------------------------------------------------------
83
+ # limit
84
+
85
+ def test_limit_forward(self) -> None:
86
+ a = np.array([1.0, np.nan, np.nan, np.nan, 5.0])
87
+ post = fill_directional(a, np.isnan(a), limit=1)
88
+ self.assertTrue(
89
+ np.array_equal(post, [1.0, 1.0, np.nan, np.nan, 5.0], equal_nan=True)
90
+ )
91
+
92
+ def test_limit_resets_per_run(self) -> None:
93
+ a = np.array([1.0, np.nan, 3.0, np.nan, np.nan, 6.0])
94
+ post = fill_directional(a, np.isnan(a), limit=1)
95
+ self.assertTrue(
96
+ np.array_equal(post, [1.0, 1.0, 3.0, 3.0, np.nan, 6.0], equal_nan=True)
97
+ )
98
+
99
+ # ------------------------------------------------------------------
100
+ # dtypes
101
+
102
+ def test_int(self) -> None:
103
+ a = np.array([5, 0, 0, 8, 0])
104
+ t = np.array([False, True, True, False, True])
105
+ self.assertEqual(fill_directional(a, t).tolist(), [5, 5, 5, 8, 8])
106
+
107
+ def test_object(self) -> None:
108
+ a = np.array([None, 'a', None, 'b', None, None], dtype=object)
109
+ t = np.array([v is None for v in a])
110
+ post = fill_directional(a, t)
111
+ self.assertEqual(post.tolist(), [None, 'a', 'a', 'b', 'b', 'b'])
112
+
113
+ def test_object_no_refcount_leak(self) -> None:
114
+ marker = object()
115
+ a = np.array([marker, None, None, marker, None], dtype=object)
116
+ t = np.array([False, True, True, False, True])
117
+ base = sys.getrefcount(marker)
118
+ for _ in range(2000):
119
+ post = fill_directional(a, t)
120
+ del post
121
+ self.assertEqual(sys.getrefcount(marker), base)
122
+
123
+ def test_datetime(self) -> None:
124
+ a = np.array(['2020-01-01', 'NaT', '2020-01-03'], dtype='datetime64[D]')
125
+ t = np.isnat(a)
126
+ post = fill_directional(a, t)
127
+ self.assertEqual(
128
+ post.tolist(),
129
+ np.array(
130
+ ['2020-01-01', '2020-01-01', '2020-01-03'], dtype='datetime64[D]'
131
+ ).tolist(),
132
+ )
133
+
134
+ # ------------------------------------------------------------------
135
+ # 2D
136
+
137
+ def test_2d_axis0(self) -> None:
138
+ a = np.array([[1.0, np.nan], [np.nan, 5.0], [3.0, np.nan]])
139
+ post = fill_directional(a, np.isnan(a), axis=0)
140
+ self.assertTrue(
141
+ np.array_equal(post, [[1.0, np.nan], [1.0, 5.0], [3.0, 5.0]], equal_nan=True)
142
+ )
143
+
144
+ def test_2d_axis1(self) -> None:
145
+ a = np.array([[1.0, np.nan, 3.0], [np.nan, np.nan, 6.0]])
146
+ post = fill_directional(a, np.isnan(a), axis=1)
147
+ self.assertTrue(
148
+ np.array_equal(
149
+ post, [[1.0, 1.0, 3.0], [np.nan, np.nan, 6.0]], equal_nan=True
150
+ )
151
+ )
152
+
153
+ # ------------------------------------------------------------------
154
+ # immutability
155
+
156
+ def test_immutable(self) -> None:
157
+ a = np.array([1.0, np.nan, 3.0])
158
+ post = fill_directional(a, np.isnan(a))
159
+ self.assertFalse(post.flags.writeable)
160
+
161
+ # ------------------------------------------------------------------
162
+ # differential vs reference across a matrix of parameters
163
+
164
+ def test_matches_reference(self) -> None:
165
+ rng = np.random.default_rng(42)
166
+ arrays_1d = (
167
+ np.round(rng.random(300), 2),
168
+ np.where(rng.random(300) < 0.5, np.nan, rng.random(300)),
169
+ rng.integers(0, 4, 300),
170
+ )
171
+ for a in arrays_1d:
172
+ t = np.isnan(a) if a.dtype.kind == 'f' else (a % 3 == 0)
173
+ for forward in (True, False):
174
+ for limit in (0, 1, 3):
175
+ post = fill_directional(a, t, forward=forward, limit=limit)
176
+ exp = reference(a, t, forward=forward, limit=limit)
177
+ self.assertTrue(
178
+ np.array_equal(post, exp, equal_nan=(a.dtype.kind == 'f'))
179
+ )
180
+ for shape in ((8, 5), (5, 8), (1, 6), (6, 1), (10, 10)):
181
+ a = np.where(rng.random(shape) < 0.35, np.nan, rng.random(shape))
182
+ t = np.isnan(a)
183
+ for axis in (0, 1):
184
+ for forward in (True, False):
185
+ for limit in (0, 2):
186
+ post = fill_directional(
187
+ a, t, forward=forward, axis=axis, limit=limit
188
+ )
189
+ exp = reference(
190
+ a, t, forward=forward, axis=axis, limit=limit
191
+ )
192
+ self.assertTrue(np.array_equal(post, exp, equal_nan=True))
193
+
194
+ # ------------------------------------------------------------------
195
+ # errors
196
+
197
+ def test_error_target_not_bool(self) -> None:
198
+ a = np.arange(3.0)
199
+ with self.assertRaises(ValueError):
200
+ fill_directional(a, np.zeros(3, dtype=int))
201
+
202
+ def test_error_shape_mismatch(self) -> None:
203
+ a = np.arange(3.0)
204
+ with self.assertRaises(ValueError):
205
+ fill_directional(a, np.zeros(4, dtype=bool))
206
+
207
+ def test_error_ndim_mismatch(self) -> None:
208
+ a = np.arange(6.0).reshape(2, 3)
209
+ with self.assertRaises(ValueError):
210
+ fill_directional(a, np.zeros(6, dtype=bool))
211
+
212
+ def test_error_bad_axis(self) -> None:
213
+ a = np.arange(6.0).reshape(2, 3)
214
+ with self.assertRaises(ValueError):
215
+ fill_directional(a, np.zeros((2, 3), dtype=bool), axis=2)
216
+
217
+ def test_error_negative_limit(self) -> None:
218
+ a = np.arange(3.0)
219
+ with self.assertRaises(ValueError):
220
+ fill_directional(a, np.zeros(3, dtype=bool), limit=-1)
221
+
222
+ def test_error_3d(self) -> None:
223
+ a = np.zeros((2, 2, 2))
224
+ with self.assertRaises(ValueError):
225
+ fill_directional(a, np.zeros((2, 2, 2), dtype=bool))
226
+
227
+
228
+ if __name__ == '__main__':
229
+ unittest.main()
@@ -1439,3 +1439,72 @@ 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))
arraykit-1.7.0/VERSION DELETED
@@ -1,2 +0,0 @@
1
- 1.7.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