arraykit 1.4.0__tar.gz → 1.5.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.4.0/arraykit.egg-info → arraykit-1.5.0}/PKG-INFO +6 -1
- {arraykit-1.4.0 → arraykit-1.5.0}/README.rst +5 -0
- arraykit-1.5.0/VERSION +2 -0
- {arraykit-1.4.0 → arraykit-1.5.0/arraykit.egg-info}/PKG-INFO +6 -1
- {arraykit-1.4.0 → arraykit-1.5.0}/arraykit.egg-info/SOURCES.txt +1 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/pyproject.toml +0 -2
- {arraykit-1.4.0 → arraykit-1.5.0}/src/__init__.py +1 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/__init__.pyi +3 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/_arraykit.c +1 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/methods.c +338 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/methods.h +3 -0
- arraykit-1.5.0/test/test_transition_slices_from_group.py +224 -0
- arraykit-1.4.0/VERSION +0 -2
- {arraykit-1.4.0 → arraykit-1.5.0}/LICENSE.txt +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/MANIFEST.in +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/arraykit.egg-info/dependency_links.txt +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/arraykit.egg-info/requires.txt +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/arraykit.egg-info/top_level.txt +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/setup.cfg +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/setup.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/array_go.c +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/array_go.h +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/array_to_tuple.c +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/array_to_tuple.h +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/auto_map.c +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/auto_map.h +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/block_index.c +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/block_index.h +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/delimited_to_arrays.c +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/delimited_to_arrays.h +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/py.typed +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/tri_map.c +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/tri_map.h +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/src/utilities.h +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_array_go.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_astype_array.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_auto_map.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_auto_map_property.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_block_index.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_delimited_to_arrays.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_delimited_to_arrays_integration.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_delimited_to_arrays_property.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_nonzero_1d.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_nonzero_1d_property.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_objectable.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_pyi.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_split_after_count.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_tri_map.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.0}/test/test_type_discovery.py +0 -0
- {arraykit-1.4.0 → arraykit-1.5.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.5.0
|
|
4
4
|
Summary: Array utilities for StaticFrame
|
|
5
5
|
Author: Christopher Ariza, Brandt Bucher, Charles Burkland
|
|
6
6
|
License: MIT
|
|
@@ -64,6 +64,11 @@ ArrayKit requires the following:
|
|
|
64
64
|
What is New in ArrayKit
|
|
65
65
|
-------------------------
|
|
66
66
|
|
|
67
|
+
1.5.0
|
|
68
|
+
............
|
|
69
|
+
Added ``transition_slices_from_group``.
|
|
70
|
+
|
|
71
|
+
|
|
67
72
|
1.4.0
|
|
68
73
|
............
|
|
69
74
|
|
arraykit-1.5.0/VERSION
ADDED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arraykit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Array utilities for StaticFrame
|
|
5
5
|
Author: Christopher Ariza, Brandt Bucher, Charles Burkland
|
|
6
6
|
License: MIT
|
|
@@ -64,6 +64,11 @@ ArrayKit requires the following:
|
|
|
64
64
|
What is New in ArrayKit
|
|
65
65
|
-------------------------
|
|
66
66
|
|
|
67
|
+
1.5.0
|
|
68
|
+
............
|
|
69
|
+
Added ``transition_slices_from_group``.
|
|
70
|
+
|
|
71
|
+
|
|
67
72
|
1.4.0
|
|
68
73
|
............
|
|
69
74
|
|
|
@@ -33,6 +33,7 @@ from ._arraykit import slice_to_unit as slice_to_unit
|
|
|
33
33
|
from ._arraykit import array_to_tuple_array as array_to_tuple_array
|
|
34
34
|
from ._arraykit import array_to_tuple_iter as array_to_tuple_iter
|
|
35
35
|
from ._arraykit import nonzero_1d as nonzero_1d
|
|
36
|
+
from ._arraykit import transition_slices_from_group as transition_slices_from_group
|
|
36
37
|
from ._arraykit import is_objectable_dt64 as is_objectable_dt64
|
|
37
38
|
from ._arraykit import is_objectable as is_objectable
|
|
38
39
|
from ._arraykit import astype_array as astype_array
|
|
@@ -227,6 +227,9 @@ def write_array_to_file(
|
|
|
227
227
|
def first_true_1d(__array: np.ndarray, *, forward: bool) -> int: ...
|
|
228
228
|
def first_true_2d(__array: np.ndarray, *, forward: bool, axis: int) -> np.ndarray: ...
|
|
229
229
|
def nonzero_1d(__array: np.ndarray, /) -> np.ndarray: ...
|
|
230
|
+
def transition_slices_from_group(
|
|
231
|
+
__group: np.ndarray, /
|
|
232
|
+
) -> tp.Tuple[tp.Iterator[slice], bool]: ...
|
|
230
233
|
def is_objectable_dt64(__array: np.ndarray, /) -> bool: ...
|
|
231
234
|
def is_objectable(__array: np.ndarray, /) -> bool: ...
|
|
232
235
|
def astype_array(__array: np.ndarray, __dtype: np.dtype | None, /) -> np.ndarray: ...
|
|
@@ -53,6 +53,7 @@ static PyMethodDef arraykit_methods[] = {
|
|
|
53
53
|
NULL},
|
|
54
54
|
{"count_iteration", count_iteration, METH_O, NULL},
|
|
55
55
|
{"nonzero_1d", nonzero_1d, METH_O, NULL},
|
|
56
|
+
{"transition_slices_from_group", transition_slices_from_group, METH_O, NULL},
|
|
56
57
|
{"is_objectable_dt64", is_objectable_dt64, METH_O, NULL},
|
|
57
58
|
{"is_objectable", is_objectable, METH_O, NULL},
|
|
58
59
|
{"astype_array", astype_array, METH_VARARGS, NULL},
|
|
@@ -253,6 +253,344 @@ nonzero_1d(PyObject *Py_UNUSED(m), PyObject *a) {
|
|
|
253
253
|
return AK_nonzero_1d(array);
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
//------------------------------------------------------------------------------
|
|
257
|
+
// transition_slices_from_group
|
|
258
|
+
|
|
259
|
+
// Returns -1 on error
|
|
260
|
+
static inline int
|
|
261
|
+
AK_append_transition_slice(PyObject* slices, npy_intp start, npy_intp stop)
|
|
262
|
+
{
|
|
263
|
+
PyObject* py_start = PyLong_FromSsize_t(start);
|
|
264
|
+
if (!py_start) {
|
|
265
|
+
return -1;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
PyObject* py_stop = NULL;
|
|
269
|
+
if (stop == -1) {
|
|
270
|
+
py_stop = Py_None;
|
|
271
|
+
Py_INCREF(py_stop);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
py_stop = PyLong_FromSsize_t(stop);
|
|
275
|
+
}
|
|
276
|
+
if (!py_stop) {
|
|
277
|
+
Py_DECREF(py_start);
|
|
278
|
+
return -1;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
PyObject* slc = PySlice_New(py_start, py_stop, Py_None);
|
|
282
|
+
Py_DECREF(py_start);
|
|
283
|
+
Py_DECREF(py_stop);
|
|
284
|
+
if (!slc) {
|
|
285
|
+
return -1;
|
|
286
|
+
}
|
|
287
|
+
int append_result = PyList_Append(slices, slc); // -1 on error
|
|
288
|
+
Py_DECREF(slc);
|
|
289
|
+
return append_result;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Return 1 if the two elements are not equal (a transition), 0 if equal, -1 on
|
|
293
|
+
// error. Equality matches numpy's elementwise `!=`: byte-exact for integral,
|
|
294
|
+
// string, and void dtypes; IEEE-aware for floats (NaN != NaN, +0.0 == -0.0);
|
|
295
|
+
// and NaT-aware for datetime64/timedelta64 (NaT != everything, including NaT).
|
|
296
|
+
// Uncommon dtypes (complex, float16, longdouble, object) fall back to a boxed
|
|
297
|
+
// Python comparison, which is correct though slower.
|
|
298
|
+
static inline int
|
|
299
|
+
AK_values_not_equal_1d(PyArrayObject* array, npy_intp left, npy_intp right)
|
|
300
|
+
{
|
|
301
|
+
npy_intp stride = PyArray_STRIDE(array, 0);
|
|
302
|
+
char* p_left = PyArray_BYTES(array) + (left * stride);
|
|
303
|
+
char* p_right = PyArray_BYTES(array) + (right * stride);
|
|
304
|
+
|
|
305
|
+
switch (PyArray_TYPE(array)) {
|
|
306
|
+
// For these dtypes numpy `!=` is exactly a byte comparison, so memcmp
|
|
307
|
+
// is both correct and the fastest option (no Python objects created).
|
|
308
|
+
case NPY_BOOL:
|
|
309
|
+
case NPY_BYTE: case NPY_UBYTE:
|
|
310
|
+
case NPY_SHORT: case NPY_USHORT:
|
|
311
|
+
case NPY_INT: case NPY_UINT:
|
|
312
|
+
case NPY_LONG: case NPY_ULONG:
|
|
313
|
+
case NPY_LONGLONG: case NPY_ULONGLONG:
|
|
314
|
+
case NPY_STRING: case NPY_UNICODE: case NPY_VOID:
|
|
315
|
+
return memcmp(p_left, p_right, (size_t)PyArray_ITEMSIZE(array)) != 0;
|
|
316
|
+
|
|
317
|
+
// Floats need IEEE semantics, which a byte compare would get wrong for
|
|
318
|
+
// NaN (distinct bits compare equal under ==) and signed zero.
|
|
319
|
+
case NPY_FLOAT: {
|
|
320
|
+
npy_float a, b;
|
|
321
|
+
memcpy(&a, p_left, sizeof a);
|
|
322
|
+
memcpy(&b, p_right, sizeof b);
|
|
323
|
+
return a != b;
|
|
324
|
+
}
|
|
325
|
+
case NPY_DOUBLE: {
|
|
326
|
+
npy_double a, b;
|
|
327
|
+
memcpy(&a, p_left, sizeof a);
|
|
328
|
+
memcpy(&b, p_right, sizeof b);
|
|
329
|
+
return a != b;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// datetime64/timedelta64 are int64 with NaT == INT64_MIN;
|
|
333
|
+
case NPY_DATETIME:
|
|
334
|
+
case NPY_TIMEDELTA: {
|
|
335
|
+
npy_int64 a, b;
|
|
336
|
+
memcpy(&a, p_left, sizeof a);
|
|
337
|
+
memcpy(&b, p_right, sizeof b);
|
|
338
|
+
if (a == NPY_DATETIME_NAT || b == NPY_DATETIME_NAT) {
|
|
339
|
+
return 1;
|
|
340
|
+
}
|
|
341
|
+
return a != b;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Object dtype and any unhandled type: compare via Python objects.
|
|
346
|
+
PyObject* left_value = PyArray_GETITEM(array, p_left);
|
|
347
|
+
if (!left_value) {
|
|
348
|
+
return -1;
|
|
349
|
+
}
|
|
350
|
+
PyObject* right_value = PyArray_GETITEM(array, p_right);
|
|
351
|
+
if (!right_value) {
|
|
352
|
+
Py_DECREF(left_value);
|
|
353
|
+
return -1;
|
|
354
|
+
}
|
|
355
|
+
int is_not_equal = PyObject_RichCompareBool(left_value, right_value, Py_NE);
|
|
356
|
+
Py_DECREF(left_value);
|
|
357
|
+
Py_DECREF(right_value);
|
|
358
|
+
return is_not_equal;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Compare two rows of a 2d array. Object arrays are compared cell-by-cell with
|
|
362
|
+
// rich equality, consistent with the 1d object path (AK_values_not_equal_1d);
|
|
363
|
+
// all other dtypes are compared bytewise, matching numpy's void-view equality.
|
|
364
|
+
static inline int
|
|
365
|
+
AK_rows_equal_2d(PyArrayObject* array, npy_intp left, npy_intp right)
|
|
366
|
+
{
|
|
367
|
+
if (PyArray_TYPE(array) == NPY_OBJECT) {
|
|
368
|
+
npy_intp cols = PyArray_DIM(array, 1);
|
|
369
|
+
for (npy_intp col = 0; col < cols; ++col) {
|
|
370
|
+
PyObject* left_value = *(PyObject**)PyArray_GETPTR2(array, left, col);
|
|
371
|
+
PyObject* right_value = *(PyObject**)PyArray_GETPTR2(array, right, col);
|
|
372
|
+
int is_equal = PyObject_RichCompareBool(left_value, right_value, Py_EQ);
|
|
373
|
+
if (is_equal <= 0) {
|
|
374
|
+
return is_equal; // 0 (not equal) or -1 (error)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return 1;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
npy_intp stride_0 = PyArray_STRIDE(array, 0);
|
|
381
|
+
npy_intp stride_1 = PyArray_STRIDE(array, 1);
|
|
382
|
+
npy_intp cols = PyArray_DIM(array, 1);
|
|
383
|
+
npy_intp itemsize = PyArray_ITEMSIZE(array);
|
|
384
|
+
|
|
385
|
+
char* p_left = PyArray_BYTES(array) + (left * stride_0);
|
|
386
|
+
char* p_right = PyArray_BYTES(array) + (right * stride_0);
|
|
387
|
+
|
|
388
|
+
if (stride_1 == itemsize) {
|
|
389
|
+
return memcmp(p_left, p_right, (size_t)(cols * itemsize)) == 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
for (npy_intp col = 0; col < cols; ++col) {
|
|
393
|
+
npy_intp offset = col * stride_1;
|
|
394
|
+
if (memcmp(p_left + offset, p_right + offset, (size_t)itemsize) != 0) {
|
|
395
|
+
return 0;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return 1;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Scan a contiguous, monomorphic 1d buffer for transitions. EQ tests whether
|
|
402
|
+
// element i equals element i-1; the inner loop runs over equal runs with no
|
|
403
|
+
// function call so the compiler can keep it tight (and vectorize the compare).
|
|
404
|
+
// On a transition, emit the run [start, i) and continue. `goto finalize` after.
|
|
405
|
+
#define AK_SCAN_TRANSITIONS(EQ) \
|
|
406
|
+
do { \
|
|
407
|
+
npy_intp i = 1; \
|
|
408
|
+
while (i < size) { \
|
|
409
|
+
while (i < size && (EQ)) { ++i; } \
|
|
410
|
+
if (i >= size) { break; } \
|
|
411
|
+
if (AK_append_transition_slice(slices, start, i)) { return -1; } \
|
|
412
|
+
start = i; \
|
|
413
|
+
++i; \
|
|
414
|
+
} \
|
|
415
|
+
} while (0)
|
|
416
|
+
|
|
417
|
+
// Append transition slices for a 1d array to `slices`. Returns 0 on success,
|
|
418
|
+
// -1 on error. Common contiguous dtypes use a hoisted, typed scan; everything
|
|
419
|
+
// else (non-contiguous, strings, void, complex, half, longdouble, object, or
|
|
420
|
+
// unusual widths) falls back to the per-element comparison.
|
|
421
|
+
static int
|
|
422
|
+
AK_fill_transition_slices_1d(PyArrayObject* working, npy_intp size, PyObject* slices)
|
|
423
|
+
{
|
|
424
|
+
npy_intp start = 0;
|
|
425
|
+
const char* base = PyArray_BYTES(working);
|
|
426
|
+
npy_intp stride = PyArray_STRIDE(working, 0);
|
|
427
|
+
npy_intp itemsize = PyArray_ITEMSIZE(working);
|
|
428
|
+
|
|
429
|
+
// stride == itemsize proves the data is packed, but the typed fast-path
|
|
430
|
+
// below casts `base` to a concrete C type and dereferences v[i] directly,
|
|
431
|
+
// which is undefined behavior on a misaligned buffer (and can SIGBUS on
|
|
432
|
+
// strict-alignment platforms). Contiguity and alignment are independent in
|
|
433
|
+
// numpy, so require PyArray_ISALIGNED explicitly; anything unaligned falls
|
|
434
|
+
// through to the memcpy/memcmp-based generic scan, which is alignment-safe.
|
|
435
|
+
if (stride == itemsize && PyArray_ISALIGNED(working)) {
|
|
436
|
+
switch (PyArray_TYPE(working)) {
|
|
437
|
+
case NPY_DOUBLE: {
|
|
438
|
+
// == honors IEEE semantics: NaN != NaN, +0.0 == -0.0
|
|
439
|
+
const npy_double* v = (const npy_double*)base;
|
|
440
|
+
AK_SCAN_TRANSITIONS(v[i] == v[i - 1]);
|
|
441
|
+
goto finalize;
|
|
442
|
+
}
|
|
443
|
+
case NPY_FLOAT: {
|
|
444
|
+
const npy_float* v = (const npy_float*)base;
|
|
445
|
+
AK_SCAN_TRANSITIONS(v[i] == v[i - 1]);
|
|
446
|
+
goto finalize;
|
|
447
|
+
}
|
|
448
|
+
case NPY_DATETIME:
|
|
449
|
+
case NPY_TIMEDELTA: {
|
|
450
|
+
// NaT (INT64_MIN) is unequal to everything, including itself
|
|
451
|
+
const npy_int64* v = (const npy_int64*)base;
|
|
452
|
+
AK_SCAN_TRANSITIONS(v[i - 1] != NPY_DATETIME_NAT
|
|
453
|
+
&& v[i] != NPY_DATETIME_NAT
|
|
454
|
+
&& v[i] == v[i - 1]);
|
|
455
|
+
goto finalize;
|
|
456
|
+
}
|
|
457
|
+
case NPY_BOOL:
|
|
458
|
+
case NPY_BYTE: case NPY_UBYTE:
|
|
459
|
+
case NPY_SHORT: case NPY_USHORT:
|
|
460
|
+
case NPY_INT: case NPY_UINT:
|
|
461
|
+
case NPY_LONG: case NPY_ULONG:
|
|
462
|
+
case NPY_LONGLONG: case NPY_ULONGLONG: {
|
|
463
|
+
// Integral dtypes: `!=` is raw-bit inequality, so compare by width.
|
|
464
|
+
switch (itemsize) {
|
|
465
|
+
case 1: { const npy_uint8* v = (const npy_uint8*)base; AK_SCAN_TRANSITIONS(v[i] == v[i - 1]); goto finalize; }
|
|
466
|
+
case 2: { const npy_uint16* v = (const npy_uint16*)base; AK_SCAN_TRANSITIONS(v[i] == v[i - 1]); goto finalize; }
|
|
467
|
+
case 4: { const npy_uint32* v = (const npy_uint32*)base; AK_SCAN_TRANSITIONS(v[i] == v[i - 1]); goto finalize; }
|
|
468
|
+
case 8: { const npy_uint64* v = (const npy_uint64*)base; AK_SCAN_TRANSITIONS(v[i] == v[i - 1]); goto finalize; }
|
|
469
|
+
default: break;
|
|
470
|
+
}
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
case NPY_OBJECT: {
|
|
474
|
+
// Read the stored PyObject* directly (borrowed, GIL held) and
|
|
475
|
+
// rich-compare, skipping PyArray_GETITEM dispatch and refcount
|
|
476
|
+
// churn. The compare can error, so this can't use the macro.
|
|
477
|
+
PyObject** v = (PyObject**)base;
|
|
478
|
+
npy_intp i = 1;
|
|
479
|
+
while (i < size) {
|
|
480
|
+
while (i < size) {
|
|
481
|
+
int eq = PyObject_RichCompareBool(v[i], v[i - 1], Py_EQ);
|
|
482
|
+
if (eq < 0) {
|
|
483
|
+
return -1;
|
|
484
|
+
}
|
|
485
|
+
if (!eq) {
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
++i;
|
|
489
|
+
}
|
|
490
|
+
if (i >= size) {
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
if (AK_append_transition_slice(slices, start, i)) {
|
|
494
|
+
return -1;
|
|
495
|
+
}
|
|
496
|
+
start = i;
|
|
497
|
+
++i;
|
|
498
|
+
}
|
|
499
|
+
goto finalize;
|
|
500
|
+
}
|
|
501
|
+
default:
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Generic fallback.
|
|
507
|
+
for (npy_intp i = 1; i < size; ++i) {
|
|
508
|
+
int is_transition = AK_values_not_equal_1d(working, i - 1, i);
|
|
509
|
+
if (is_transition < 0) {
|
|
510
|
+
return -1;
|
|
511
|
+
}
|
|
512
|
+
if (is_transition) {
|
|
513
|
+
if (AK_append_transition_slice(slices, start, i)) {
|
|
514
|
+
return -1;
|
|
515
|
+
}
|
|
516
|
+
start = i;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
finalize:
|
|
521
|
+
if (start < size) {
|
|
522
|
+
if (AK_append_transition_slice(slices, start, -1)) {
|
|
523
|
+
return -1;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
#undef AK_SCAN_TRANSITIONS
|
|
529
|
+
|
|
530
|
+
PyObject *
|
|
531
|
+
transition_slices_from_group(PyObject *Py_UNUSED(m), PyObject *a)
|
|
532
|
+
{
|
|
533
|
+
AK_CHECK_NUMPY_ARRAY_1D_2D(a);
|
|
534
|
+
PyArrayObject* group = (PyArrayObject*)a;
|
|
535
|
+
bool group_to_tuple = PyArray_NDIM(group) == 2;
|
|
536
|
+
npy_intp size = PyArray_DIM(group, 0);
|
|
537
|
+
|
|
538
|
+
PyArrayObject* working = group;
|
|
539
|
+
Py_INCREF(working);
|
|
540
|
+
|
|
541
|
+
PyObject* slices = PyList_New(0);
|
|
542
|
+
if (!slices) {
|
|
543
|
+
Py_DECREF(working);
|
|
544
|
+
return NULL;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (!group_to_tuple) {
|
|
548
|
+
if (AK_fill_transition_slices_1d(working, size, slices)) {
|
|
549
|
+
Py_DECREF(working);
|
|
550
|
+
Py_DECREF(slices);
|
|
551
|
+
return NULL;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
npy_intp start = 0;
|
|
556
|
+
for (npy_intp i = 1; i < size; ++i) {
|
|
557
|
+
int is_equal = AK_rows_equal_2d(working, i - 1, i);
|
|
558
|
+
if (is_equal < 0) {
|
|
559
|
+
Py_DECREF(working);
|
|
560
|
+
Py_DECREF(slices);
|
|
561
|
+
return NULL;
|
|
562
|
+
}
|
|
563
|
+
if (!is_equal) {
|
|
564
|
+
if (AK_append_transition_slice(slices, start, i)) {
|
|
565
|
+
Py_DECREF(working);
|
|
566
|
+
Py_DECREF(slices);
|
|
567
|
+
return NULL;
|
|
568
|
+
}
|
|
569
|
+
start = i;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (start < size) {
|
|
573
|
+
if (AK_append_transition_slice(slices, start, -1)) {
|
|
574
|
+
Py_DECREF(working);
|
|
575
|
+
Py_DECREF(slices);
|
|
576
|
+
return NULL;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
Py_DECREF(working);
|
|
581
|
+
|
|
582
|
+
PyObject* slices_iter = PyObject_GetIter(slices);
|
|
583
|
+
Py_DECREF(slices);
|
|
584
|
+
if (!slices_iter) {
|
|
585
|
+
return NULL;
|
|
586
|
+
}
|
|
587
|
+
PyObject* result = PyTuple_Pack(2, slices_iter, group_to_tuple ? Py_True : Py_False);
|
|
588
|
+
Py_DECREF(slices_iter);
|
|
589
|
+
return result;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
//------------------------------------------------------------------------------
|
|
256
594
|
PyObject*
|
|
257
595
|
is_objectable_dt64(PyObject *m, PyObject *a) {
|
|
258
596
|
AK_CHECK_NUMPY_ARRAY(a);
|
|
@@ -51,6 +51,9 @@ resolve_dtype_iter(PyObject *Py_UNUSED(m), PyObject *arg);
|
|
|
51
51
|
PyObject *
|
|
52
52
|
nonzero_1d(PyObject *Py_UNUSED(m), PyObject *a);
|
|
53
53
|
|
|
54
|
+
PyObject *
|
|
55
|
+
transition_slices_from_group(PyObject *Py_UNUSED(m), PyObject *a);
|
|
56
|
+
|
|
54
57
|
PyObject *
|
|
55
58
|
is_objectable_dt64(PyObject *m, PyObject *a);
|
|
56
59
|
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from arraykit import transition_slices_from_group
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def slices_to_pairs(slices):
|
|
9
|
+
return [(s.start, s.stop, s.step) for s in slices]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestUnit(unittest.TestCase):
|
|
13
|
+
def test_transition_slices_from_group_1d_a(self) -> None:
|
|
14
|
+
group = np.array([10, 10, 10, 20, 20, 30])
|
|
15
|
+
slices, group_to_tuple = transition_slices_from_group(group)
|
|
16
|
+
self.assertFalse(group_to_tuple)
|
|
17
|
+
self.assertEqual(
|
|
18
|
+
slices_to_pairs(slices),
|
|
19
|
+
[(0, 3, None), (3, 5, None), (5, None, None)],
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def test_transition_slices_from_group_1d_object_a(self) -> None:
|
|
23
|
+
group = np.array(['a', 'a', 'b', 'b', 'c'], dtype=object)
|
|
24
|
+
slices, group_to_tuple = transition_slices_from_group(group)
|
|
25
|
+
self.assertFalse(group_to_tuple)
|
|
26
|
+
self.assertEqual(
|
|
27
|
+
slices_to_pairs(slices),
|
|
28
|
+
[(0, 2, None), (2, 4, None), (4, None, None)],
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def test_transition_slices_from_group_1d_unaligned(self) -> None:
|
|
32
|
+
# A contiguous (stride == itemsize) but *unaligned* buffer must not take
|
|
33
|
+
# the typed fast-path, whose pointer-cast dereferences are UB on
|
|
34
|
+
# misaligned data; it should fall through to the alignment-safe scan and
|
|
35
|
+
# still produce results identical to an aligned copy.
|
|
36
|
+
def make_unaligned(values, dtype):
|
|
37
|
+
dt = np.dtype(dtype)
|
|
38
|
+
n = len(values)
|
|
39
|
+
raw = np.empty(n * dt.itemsize + dt.itemsize, dtype=np.uint8)
|
|
40
|
+
# view starting one byte in -> contiguous but misaligned
|
|
41
|
+
a = raw[1 : 1 + n * dt.itemsize].view(dt)
|
|
42
|
+
a[:] = values
|
|
43
|
+
return a
|
|
44
|
+
|
|
45
|
+
for dtype, values in (
|
|
46
|
+
(np.float64, [1.0, 1.0, 2.0, 2.0, 2.0, 3.0]),
|
|
47
|
+
(np.int64, [10, 10, 10, 20, 20, 30]),
|
|
48
|
+
(np.int32, [5, 5, 6, 7, 7]),
|
|
49
|
+
):
|
|
50
|
+
a = make_unaligned(values, dtype)
|
|
51
|
+
self.assertTrue(a.flags.c_contiguous)
|
|
52
|
+
self.assertFalse(a.flags.aligned)
|
|
53
|
+
ref = np.array(values, dtype=dtype) # fresh, aligned
|
|
54
|
+
|
|
55
|
+
slices, group_to_tuple = transition_slices_from_group(a)
|
|
56
|
+
ref_slices, _ = transition_slices_from_group(ref)
|
|
57
|
+
self.assertFalse(group_to_tuple)
|
|
58
|
+
self.assertEqual(slices_to_pairs(slices), slices_to_pairs(ref_slices))
|
|
59
|
+
|
|
60
|
+
def test_transition_slices_from_group_2d_a(self) -> None:
|
|
61
|
+
group = np.array(
|
|
62
|
+
[
|
|
63
|
+
[1, 2],
|
|
64
|
+
[1, 2],
|
|
65
|
+
[1, 3],
|
|
66
|
+
[1, 3],
|
|
67
|
+
[2, 4],
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
slices, group_to_tuple = transition_slices_from_group(group)
|
|
71
|
+
self.assertTrue(group_to_tuple)
|
|
72
|
+
self.assertEqual(
|
|
73
|
+
slices_to_pairs(slices),
|
|
74
|
+
[(0, 2, None), (2, 4, None), (4, None, None)],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def test_transition_slices_from_group_2d_mixed_types_a(self) -> None:
|
|
78
|
+
# object rows are compared by value (rich equality), consistent with the
|
|
79
|
+
# 1d object path: 1, '1', and 2 are all distinct, so no rows merge
|
|
80
|
+
group = np.array([[1], ['1'], [2]], dtype=object)
|
|
81
|
+
self.assertNotEqual(group[0, 0], group[1, 0])
|
|
82
|
+
slices, group_to_tuple = transition_slices_from_group(group)
|
|
83
|
+
self.assertTrue(group_to_tuple)
|
|
84
|
+
self.assertEqual(
|
|
85
|
+
slices_to_pairs(slices),
|
|
86
|
+
[(0, 1, None), (1, 2, None), (2, None, None)],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def test_transition_slices_from_group_2d_object_value_equality(self) -> None:
|
|
90
|
+
# equal values across rows merge even when not the same object;
|
|
91
|
+
# 1 == 1.0 by value, so rows 0 and 1 form one group
|
|
92
|
+
group = np.array([[1, 2], [1.0, 2], [1, 3]], dtype=object)
|
|
93
|
+
slices, group_to_tuple = transition_slices_from_group(group)
|
|
94
|
+
self.assertTrue(group_to_tuple)
|
|
95
|
+
self.assertEqual(
|
|
96
|
+
slices_to_pairs(slices),
|
|
97
|
+
[(0, 2, None), (2, None, None)],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def test_transition_slices_from_group_2d_non_contiguous_a(self) -> None:
|
|
101
|
+
group = np.array(
|
|
102
|
+
[
|
|
103
|
+
[1, 1, 2, 2],
|
|
104
|
+
[1, 1, 2, 2],
|
|
105
|
+
],
|
|
106
|
+
dtype=np.int64,
|
|
107
|
+
).T
|
|
108
|
+
self.assertFalse(group.flags.c_contiguous)
|
|
109
|
+
slices, group_to_tuple = transition_slices_from_group(group)
|
|
110
|
+
self.assertTrue(group_to_tuple)
|
|
111
|
+
self.assertEqual(
|
|
112
|
+
slices_to_pairs(slices),
|
|
113
|
+
[(0, 2, None), (2, None, None)],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def test_transition_slices_from_group_empty_a(self) -> None:
|
|
117
|
+
slices, group_to_tuple = transition_slices_from_group(np.array([], dtype=int))
|
|
118
|
+
self.assertFalse(group_to_tuple)
|
|
119
|
+
self.assertEqual(slices_to_pairs(slices), [])
|
|
120
|
+
|
|
121
|
+
def test_transition_slices_from_group_invalid_ndim_a(self) -> None:
|
|
122
|
+
with self.assertRaises(NotImplementedError):
|
|
123
|
+
transition_slices_from_group(np.arange(8).reshape(2, 2, 2))
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# dtype-specific 1d paths (typed fast scans)
|
|
127
|
+
|
|
128
|
+
def test_transition_slices_from_group_1d_dtypes(self) -> None:
|
|
129
|
+
# the integral fast paths dispatch by width; cover each
|
|
130
|
+
for dtype in (
|
|
131
|
+
np.int8,
|
|
132
|
+
np.int16,
|
|
133
|
+
np.int32,
|
|
134
|
+
np.int64,
|
|
135
|
+
np.uint8,
|
|
136
|
+
np.uint16,
|
|
137
|
+
np.uint32,
|
|
138
|
+
np.uint64,
|
|
139
|
+
np.float32,
|
|
140
|
+
np.float64,
|
|
141
|
+
):
|
|
142
|
+
group = np.array([10, 10, 10, 20, 20, 30], dtype=dtype)
|
|
143
|
+
slices, group_to_tuple = transition_slices_from_group(group)
|
|
144
|
+
self.assertFalse(group_to_tuple)
|
|
145
|
+
self.assertEqual(
|
|
146
|
+
slices_to_pairs(slices),
|
|
147
|
+
[(0, 3, None), (3, 5, None), (5, None, None)],
|
|
148
|
+
dtype,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def test_transition_slices_from_group_1d_bool(self) -> None:
|
|
152
|
+
group = np.array([True, True, False, False, True])
|
|
153
|
+
slices, _ = transition_slices_from_group(group)
|
|
154
|
+
self.assertEqual(
|
|
155
|
+
slices_to_pairs(slices),
|
|
156
|
+
[(0, 2, None), (2, 4, None), (4, None, None)],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def test_transition_slices_from_group_1d_float_nan(self) -> None:
|
|
160
|
+
# NaN != NaN, so each adjacent NaN is its own group (matches numpy `!=`)
|
|
161
|
+
group = np.array([1.0, np.nan, np.nan, 2.0])
|
|
162
|
+
slices, _ = transition_slices_from_group(group)
|
|
163
|
+
self.assertEqual(
|
|
164
|
+
slices_to_pairs(slices),
|
|
165
|
+
[(0, 1, None), (1, 2, None), (2, 3, None), (3, None, None)],
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def test_transition_slices_from_group_1d_float_signed_zero(self) -> None:
|
|
169
|
+
# +0.0 == -0.0, so no transition (a byte compare would wrongly split)
|
|
170
|
+
group = np.array([0.0, -0.0, 0.0])
|
|
171
|
+
slices, _ = transition_slices_from_group(group)
|
|
172
|
+
self.assertEqual(slices_to_pairs(slices), [(0, None, None)])
|
|
173
|
+
|
|
174
|
+
def test_transition_slices_from_group_1d_datetime_nat(self) -> None:
|
|
175
|
+
# NaT is unequal to everything, including itself
|
|
176
|
+
d = np.datetime64('2020-01-01')
|
|
177
|
+
nat = np.datetime64('NaT')
|
|
178
|
+
group = np.array([d, nat, nat, d])
|
|
179
|
+
slices, _ = transition_slices_from_group(group)
|
|
180
|
+
self.assertEqual(
|
|
181
|
+
slices_to_pairs(slices),
|
|
182
|
+
[(0, 1, None), (1, 2, None), (2, 3, None), (3, None, None)],
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def test_transition_slices_from_group_1d_timedelta_nat(self) -> None:
|
|
186
|
+
t = np.timedelta64(5, 'D')
|
|
187
|
+
nat = np.timedelta64('NaT')
|
|
188
|
+
group = np.array([t, t, nat, nat])
|
|
189
|
+
slices, _ = transition_slices_from_group(group)
|
|
190
|
+
self.assertEqual(
|
|
191
|
+
slices_to_pairs(slices),
|
|
192
|
+
[(0, 2, None), (2, 3, None), (3, None, None)],
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def test_transition_slices_from_group_1d_str(self) -> None:
|
|
196
|
+
group = np.array(['a', 'a', 'bb', 'b', 'b'])
|
|
197
|
+
slices, _ = transition_slices_from_group(group)
|
|
198
|
+
self.assertEqual(
|
|
199
|
+
slices_to_pairs(slices),
|
|
200
|
+
[(0, 2, None), (2, 3, None), (3, None, None)],
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def test_transition_slices_from_group_1d_non_contiguous(self) -> None:
|
|
204
|
+
# strided 1d view exercises the generic fallback (stride != itemsize)
|
|
205
|
+
# underlying buffer is [1,1,2,2,3,3]; the [::2] view is [1,2,3], so a
|
|
206
|
+
# naive contiguous read would give the wrong groups
|
|
207
|
+
group = np.array([1, 1, 2, 2, 3, 3], dtype=np.int64)[::2]
|
|
208
|
+
self.assertFalse(group.flags.c_contiguous)
|
|
209
|
+
slices, _ = transition_slices_from_group(group)
|
|
210
|
+
self.assertEqual(
|
|
211
|
+
slices_to_pairs(slices),
|
|
212
|
+
[(0, 1, None), (1, 2, None), (2, None, None)],
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def test_transition_slices_from_group_2d_float_nan(self) -> None:
|
|
216
|
+
# 2d compares rows bytewise (via void view), so identical-bit NaN rows
|
|
217
|
+
# are equal -- unlike the 1d float path
|
|
218
|
+
group = np.array([[np.nan], [np.nan], [1.0]])
|
|
219
|
+
slices, group_to_tuple = transition_slices_from_group(group)
|
|
220
|
+
self.assertTrue(group_to_tuple)
|
|
221
|
+
self.assertEqual(
|
|
222
|
+
slices_to_pairs(slices),
|
|
223
|
+
[(0, 2, None), (2, None, None)],
|
|
224
|
+
)
|
arraykit-1.4.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
|