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.
Files changed (50) hide show
  1. {arraykit-1.4.0/arraykit.egg-info → arraykit-1.5.0}/PKG-INFO +6 -1
  2. {arraykit-1.4.0 → arraykit-1.5.0}/README.rst +5 -0
  3. arraykit-1.5.0/VERSION +2 -0
  4. {arraykit-1.4.0 → arraykit-1.5.0/arraykit.egg-info}/PKG-INFO +6 -1
  5. {arraykit-1.4.0 → arraykit-1.5.0}/arraykit.egg-info/SOURCES.txt +1 -0
  6. {arraykit-1.4.0 → arraykit-1.5.0}/pyproject.toml +0 -2
  7. {arraykit-1.4.0 → arraykit-1.5.0}/src/__init__.py +1 -0
  8. {arraykit-1.4.0 → arraykit-1.5.0}/src/__init__.pyi +3 -0
  9. {arraykit-1.4.0 → arraykit-1.5.0}/src/_arraykit.c +1 -0
  10. {arraykit-1.4.0 → arraykit-1.5.0}/src/methods.c +338 -0
  11. {arraykit-1.4.0 → arraykit-1.5.0}/src/methods.h +3 -0
  12. arraykit-1.5.0/test/test_transition_slices_from_group.py +224 -0
  13. arraykit-1.4.0/VERSION +0 -2
  14. {arraykit-1.4.0 → arraykit-1.5.0}/LICENSE.txt +0 -0
  15. {arraykit-1.4.0 → arraykit-1.5.0}/MANIFEST.in +0 -0
  16. {arraykit-1.4.0 → arraykit-1.5.0}/arraykit.egg-info/dependency_links.txt +0 -0
  17. {arraykit-1.4.0 → arraykit-1.5.0}/arraykit.egg-info/requires.txt +0 -0
  18. {arraykit-1.4.0 → arraykit-1.5.0}/arraykit.egg-info/top_level.txt +0 -0
  19. {arraykit-1.4.0 → arraykit-1.5.0}/setup.cfg +0 -0
  20. {arraykit-1.4.0 → arraykit-1.5.0}/setup.py +0 -0
  21. {arraykit-1.4.0 → arraykit-1.5.0}/src/array_go.c +0 -0
  22. {arraykit-1.4.0 → arraykit-1.5.0}/src/array_go.h +0 -0
  23. {arraykit-1.4.0 → arraykit-1.5.0}/src/array_to_tuple.c +0 -0
  24. {arraykit-1.4.0 → arraykit-1.5.0}/src/array_to_tuple.h +0 -0
  25. {arraykit-1.4.0 → arraykit-1.5.0}/src/auto_map.c +0 -0
  26. {arraykit-1.4.0 → arraykit-1.5.0}/src/auto_map.h +0 -0
  27. {arraykit-1.4.0 → arraykit-1.5.0}/src/block_index.c +0 -0
  28. {arraykit-1.4.0 → arraykit-1.5.0}/src/block_index.h +0 -0
  29. {arraykit-1.4.0 → arraykit-1.5.0}/src/delimited_to_arrays.c +0 -0
  30. {arraykit-1.4.0 → arraykit-1.5.0}/src/delimited_to_arrays.h +0 -0
  31. {arraykit-1.4.0 → arraykit-1.5.0}/src/py.typed +0 -0
  32. {arraykit-1.4.0 → arraykit-1.5.0}/src/tri_map.c +0 -0
  33. {arraykit-1.4.0 → arraykit-1.5.0}/src/tri_map.h +0 -0
  34. {arraykit-1.4.0 → arraykit-1.5.0}/src/utilities.h +0 -0
  35. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_array_go.py +0 -0
  36. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_astype_array.py +0 -0
  37. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_auto_map.py +0 -0
  38. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_auto_map_property.py +0 -0
  39. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_block_index.py +0 -0
  40. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_delimited_to_arrays.py +0 -0
  41. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_delimited_to_arrays_integration.py +0 -0
  42. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_delimited_to_arrays_property.py +0 -0
  43. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_nonzero_1d.py +0 -0
  44. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_nonzero_1d_property.py +0 -0
  45. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_objectable.py +0 -0
  46. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_pyi.py +0 -0
  47. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_split_after_count.py +0 -0
  48. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_tri_map.py +0 -0
  49. {arraykit-1.4.0 → arraykit-1.5.0}/test/test_type_discovery.py +0 -0
  50. {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.4.0
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
 
@@ -35,6 +35,11 @@ ArrayKit requires the following:
35
35
  What is New in ArrayKit
36
36
  -------------------------
37
37
 
38
+ 1.5.0
39
+ ............
40
+ Added ``transition_slices_from_group``.
41
+
42
+
38
43
  1.4.0
39
44
  ............
40
45
 
arraykit-1.5.0/VERSION ADDED
@@ -0,0 +1,2 @@
1
+ 1.5.0
2
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arraykit
3
- Version: 1.4.0
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
 
@@ -41,6 +41,7 @@ test/test_nonzero_1d_property.py
41
41
  test/test_objectable.py
42
42
  test/test_pyi.py
43
43
  test/test_split_after_count.py
44
+ test/test_transition_slices_from_group.py
44
45
  test/test_tri_map.py
45
46
  test/test_type_discovery.py
46
47
  test/test_util.py
@@ -74,5 +74,3 @@ skip-magic-trailing-comma = false
74
74
  line-ending = "auto"
75
75
  docstring-code-format = true
76
76
  docstring-code-line-length = "dynamic"
77
-
78
-
@@ -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
@@ -1,2 +0,0 @@
1
- 1.4.0
2
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes