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.
- {arraykit-1.7.0/arraykit.egg-info → arraykit-1.9.0}/PKG-INFO +13 -1
- {arraykit-1.7.0 → arraykit-1.9.0}/README.rst +12 -0
- arraykit-1.9.0/VERSION +2 -0
- {arraykit-1.7.0 → arraykit-1.9.0/arraykit.egg-info}/PKG-INFO +13 -1
- {arraykit-1.7.0 → arraykit-1.9.0}/arraykit.egg-info/SOURCES.txt +1 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/__init__.py +1 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/__init__.pyi +10 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/_arraykit.c +4 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/auto_map.c +204 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/methods.c +157 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/methods.h +3 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/tri_map.c +42 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_auto_map.py +103 -0
- arraykit-1.9.0/test/test_fill_directional.py +229 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_tri_map.py +69 -0
- arraykit-1.7.0/VERSION +0 -2
- {arraykit-1.7.0 → arraykit-1.9.0}/LICENSE.txt +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/MANIFEST.in +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/arraykit.egg-info/dependency_links.txt +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/arraykit.egg-info/requires.txt +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/arraykit.egg-info/top_level.txt +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/pyproject.toml +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/setup.cfg +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/setup.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/array_go.c +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/array_go.h +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/array_to_tuple.c +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/array_to_tuple.h +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/auto_map.h +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/block_index.c +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/block_index.h +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/delimited_to_arrays.c +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/delimited_to_arrays.h +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/py.typed +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/tri_map.h +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/src/utilities.h +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_array_go.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_astype_array.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_auto_map_property.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_block_index.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_delimited_to_arrays.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_delimited_to_arrays_integration.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_delimited_to_arrays_property.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_factorize.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_group_ordering.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_nonzero_1d.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_nonzero_1d_property.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_objectable.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_pyi.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_split_after_count.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_transition_slices_from_group.py +0 -0
- {arraykit-1.7.0 → arraykit-1.9.0}/test/test_type_discovery.py +0 -0
- {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.
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arraykit
|
|
3
|
-
Version: 1.
|
|
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: ...
|
|
@@ -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
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|