nulldtype 0.1.0__tar.gz → 0.1.1__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.
- {nulldtype-0.1.0/nulldtype.egg-info → nulldtype-0.1.1}/PKG-INFO +6 -4
- {nulldtype-0.1.0 → nulldtype-0.1.1}/README.md +5 -3
- {nulldtype-0.1.0 → nulldtype-0.1.1/nulldtype.egg-info}/PKG-INFO +6 -4
- {nulldtype-0.1.0 → nulldtype-0.1.1}/pyproject.toml +1 -1
- nulldtype-0.1.1/setup.py +31 -0
- {nulldtype-0.1.0 → nulldtype-0.1.1}/src/nulldtype.c +516 -140
- {nulldtype-0.1.0 → nulldtype-0.1.1}/test_basic.py +222 -0
- nulldtype-0.1.0/setup.py +0 -11
- {nulldtype-0.1.0 → nulldtype-0.1.1}/LICENSE.txt +0 -0
- {nulldtype-0.1.0 → nulldtype-0.1.1}/MANIFEST.in +0 -0
- {nulldtype-0.1.0 → nulldtype-0.1.1}/nulldtype.egg-info/SOURCES.txt +0 -0
- {nulldtype-0.1.0 → nulldtype-0.1.1}/nulldtype.egg-info/dependency_links.txt +0 -0
- {nulldtype-0.1.0 → nulldtype-0.1.1}/nulldtype.egg-info/requires.txt +0 -0
- {nulldtype-0.1.0 → nulldtype-0.1.1}/nulldtype.egg-info/top_level.txt +0 -0
- {nulldtype-0.1.0 → nulldtype-0.1.1}/nulldtype.py +0 -0
- {nulldtype-0.1.0 → nulldtype-0.1.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nulldtype
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Missing values as a NumPy DType: NA stored as a reserved bit pattern of the wrapped type
|
|
5
5
|
License-Expression: BSD-3-Clause
|
|
6
6
|
Project-URL: Source, https://github.com/tuan2k33/NullableDType
|
|
@@ -44,6 +44,7 @@ two layers compose. In the terms of the 2012 NumPy discussion, this is
|
|
|
44
44
|
| `LAYOUTS.md` | the NA pattern of every type, and what each gives up |
|
|
45
45
|
| `VS-NUMPY-MA.md` | all 218 names in `numpy.ma.__all__` that numpy 2 still spells that way, side by side with `nd`, with real calls and results |
|
|
46
46
|
| `DISCUSSION.md` | how it works and why: the borrowed loops, performance, the bugs found on the way, leak checking |
|
|
47
|
+
| `NUMPY-MA-PITFALLS.md` | `numpy.ma` behaviours that leak, mislead or raise, with causes and issue links |
|
|
47
48
|
| `NUMPY-PATCHES.md` | where numpy's array functions fail on Nullable, and patches that leave plain arrays unchanged |
|
|
48
49
|
| `AGENTS.md` | notes for coding agents working on the repo |
|
|
49
50
|
| `archive/flag-layout/` | the earlier two-layout version, frozen |
|
|
@@ -67,7 +68,7 @@ builds and runs pytest (extra arguments go to pytest). They use `python3`;
|
|
|
67
68
|
set `PYTHON`, and `NUMPY_SITE` for a numpy dev tree that is not installed, or
|
|
68
69
|
put both in an uncommitted `local.env`.
|
|
69
70
|
|
|
70
|
-
The suite has
|
|
71
|
+
The suite has 637 tests plus 2 expected failures that pin known gaps. It also
|
|
71
72
|
passes on an ASAN + UBSAN build, which CI runs on every push.
|
|
72
73
|
|
|
73
74
|
## What works
|
|
@@ -128,6 +129,7 @@ nd.Nullable(">i4") # Nullable(int32) — stored in native byte order
|
|
|
128
129
|
| Results follow numpy's rules | `resolve` asks the wrapped ufunc, so `Nullable[i8] / Nullable[i8]` is `Nullable[f8]` and `Nullable[i8] + Nullable[f8]` is `Nullable[f8]` |
|
|
129
130
|
| Mixed operands | `Nullable[T] op T` and Python scalars, both ways round, through promoters |
|
|
130
131
|
| Comparisons | answer `Nullable[bool]`; `NA == NA` is `NA`, not `True` |
|
|
132
|
+
| Answers that do not depend on the gap | `1 ** NA` and `NA ** 0` are 1, `hypot(inf, NA)` is inf, `heaviside(3, NA)` is 1 — as in R and pandas; `fmax(NA, 2)` and `copysign(1, NA)` do depend on it and are NA |
|
|
131
133
|
| Three-valued logic | `logical_and/or/xor` and `&`, `\|`, `^` on `Nullable[bool]` are Kleene: `NA & False = False`, `NA \| True = True`; on ints `&`, `\|`, `^` propagate |
|
|
132
134
|
| Reductions and `accumulate` | propagate by default, like R's `na.rm = FALSE`, over several axes at once too; `cumsum` is NA from the first gap on |
|
|
133
135
|
| `argmax`, `argmin` | the position of the first gap, numpy's rule for NaN, so `a[a.argmax()]` is NA exactly when `a.max()` is |
|
|
@@ -241,8 +243,8 @@ plain arrays, is in `NUMPY-PATCHES.md`.
|
|
|
241
243
|
widening to `int64`.
|
|
242
244
|
3. **A real `longdouble`** through `numpy-quaddtype` instead of substituting
|
|
243
245
|
`float64`.
|
|
244
|
-
4. **
|
|
245
|
-
visible on small arrays.
|
|
246
|
+
4. **Cache the loop lookups** — about 1.4 µs of the ~2.5 µs fixed cost per
|
|
247
|
+
operation, most visible on small arrays.
|
|
246
248
|
|
|
247
249
|
## Settled design notes
|
|
248
250
|
|
|
@@ -26,6 +26,7 @@ two layers compose. In the terms of the 2012 NumPy discussion, this is
|
|
|
26
26
|
| `LAYOUTS.md` | the NA pattern of every type, and what each gives up |
|
|
27
27
|
| `VS-NUMPY-MA.md` | all 218 names in `numpy.ma.__all__` that numpy 2 still spells that way, side by side with `nd`, with real calls and results |
|
|
28
28
|
| `DISCUSSION.md` | how it works and why: the borrowed loops, performance, the bugs found on the way, leak checking |
|
|
29
|
+
| `NUMPY-MA-PITFALLS.md` | `numpy.ma` behaviours that leak, mislead or raise, with causes and issue links |
|
|
29
30
|
| `NUMPY-PATCHES.md` | where numpy's array functions fail on Nullable, and patches that leave plain arrays unchanged |
|
|
30
31
|
| `AGENTS.md` | notes for coding agents working on the repo |
|
|
31
32
|
| `archive/flag-layout/` | the earlier two-layout version, frozen |
|
|
@@ -49,7 +50,7 @@ builds and runs pytest (extra arguments go to pytest). They use `python3`;
|
|
|
49
50
|
set `PYTHON`, and `NUMPY_SITE` for a numpy dev tree that is not installed, or
|
|
50
51
|
put both in an uncommitted `local.env`.
|
|
51
52
|
|
|
52
|
-
The suite has
|
|
53
|
+
The suite has 637 tests plus 2 expected failures that pin known gaps. It also
|
|
53
54
|
passes on an ASAN + UBSAN build, which CI runs on every push.
|
|
54
55
|
|
|
55
56
|
## What works
|
|
@@ -110,6 +111,7 @@ nd.Nullable(">i4") # Nullable(int32) — stored in native byte order
|
|
|
110
111
|
| Results follow numpy's rules | `resolve` asks the wrapped ufunc, so `Nullable[i8] / Nullable[i8]` is `Nullable[f8]` and `Nullable[i8] + Nullable[f8]` is `Nullable[f8]` |
|
|
111
112
|
| Mixed operands | `Nullable[T] op T` and Python scalars, both ways round, through promoters |
|
|
112
113
|
| Comparisons | answer `Nullable[bool]`; `NA == NA` is `NA`, not `True` |
|
|
114
|
+
| Answers that do not depend on the gap | `1 ** NA` and `NA ** 0` are 1, `hypot(inf, NA)` is inf, `heaviside(3, NA)` is 1 — as in R and pandas; `fmax(NA, 2)` and `copysign(1, NA)` do depend on it and are NA |
|
|
113
115
|
| Three-valued logic | `logical_and/or/xor` and `&`, `\|`, `^` on `Nullable[bool]` are Kleene: `NA & False = False`, `NA \| True = True`; on ints `&`, `\|`, `^` propagate |
|
|
114
116
|
| Reductions and `accumulate` | propagate by default, like R's `na.rm = FALSE`, over several axes at once too; `cumsum` is NA from the first gap on |
|
|
115
117
|
| `argmax`, `argmin` | the position of the first gap, numpy's rule for NaN, so `a[a.argmax()]` is NA exactly when `a.max()` is |
|
|
@@ -223,8 +225,8 @@ plain arrays, is in `NUMPY-PATCHES.md`.
|
|
|
223
225
|
widening to `int64`.
|
|
224
226
|
3. **A real `longdouble`** through `numpy-quaddtype` instead of substituting
|
|
225
227
|
`float64`.
|
|
226
|
-
4. **
|
|
227
|
-
visible on small arrays.
|
|
228
|
+
4. **Cache the loop lookups** — about 1.4 µs of the ~2.5 µs fixed cost per
|
|
229
|
+
operation, most visible on small arrays.
|
|
228
230
|
|
|
229
231
|
## Settled design notes
|
|
230
232
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nulldtype
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Missing values as a NumPy DType: NA stored as a reserved bit pattern of the wrapped type
|
|
5
5
|
License-Expression: BSD-3-Clause
|
|
6
6
|
Project-URL: Source, https://github.com/tuan2k33/NullableDType
|
|
@@ -44,6 +44,7 @@ two layers compose. In the terms of the 2012 NumPy discussion, this is
|
|
|
44
44
|
| `LAYOUTS.md` | the NA pattern of every type, and what each gives up |
|
|
45
45
|
| `VS-NUMPY-MA.md` | all 218 names in `numpy.ma.__all__` that numpy 2 still spells that way, side by side with `nd`, with real calls and results |
|
|
46
46
|
| `DISCUSSION.md` | how it works and why: the borrowed loops, performance, the bugs found on the way, leak checking |
|
|
47
|
+
| `NUMPY-MA-PITFALLS.md` | `numpy.ma` behaviours that leak, mislead or raise, with causes and issue links |
|
|
47
48
|
| `NUMPY-PATCHES.md` | where numpy's array functions fail on Nullable, and patches that leave plain arrays unchanged |
|
|
48
49
|
| `AGENTS.md` | notes for coding agents working on the repo |
|
|
49
50
|
| `archive/flag-layout/` | the earlier two-layout version, frozen |
|
|
@@ -67,7 +68,7 @@ builds and runs pytest (extra arguments go to pytest). They use `python3`;
|
|
|
67
68
|
set `PYTHON`, and `NUMPY_SITE` for a numpy dev tree that is not installed, or
|
|
68
69
|
put both in an uncommitted `local.env`.
|
|
69
70
|
|
|
70
|
-
The suite has
|
|
71
|
+
The suite has 637 tests plus 2 expected failures that pin known gaps. It also
|
|
71
72
|
passes on an ASAN + UBSAN build, which CI runs on every push.
|
|
72
73
|
|
|
73
74
|
## What works
|
|
@@ -128,6 +129,7 @@ nd.Nullable(">i4") # Nullable(int32) — stored in native byte order
|
|
|
128
129
|
| Results follow numpy's rules | `resolve` asks the wrapped ufunc, so `Nullable[i8] / Nullable[i8]` is `Nullable[f8]` and `Nullable[i8] + Nullable[f8]` is `Nullable[f8]` |
|
|
129
130
|
| Mixed operands | `Nullable[T] op T` and Python scalars, both ways round, through promoters |
|
|
130
131
|
| Comparisons | answer `Nullable[bool]`; `NA == NA` is `NA`, not `True` |
|
|
132
|
+
| Answers that do not depend on the gap | `1 ** NA` and `NA ** 0` are 1, `hypot(inf, NA)` is inf, `heaviside(3, NA)` is 1 — as in R and pandas; `fmax(NA, 2)` and `copysign(1, NA)` do depend on it and are NA |
|
|
131
133
|
| Three-valued logic | `logical_and/or/xor` and `&`, `\|`, `^` on `Nullable[bool]` are Kleene: `NA & False = False`, `NA \| True = True`; on ints `&`, `\|`, `^` propagate |
|
|
132
134
|
| Reductions and `accumulate` | propagate by default, like R's `na.rm = FALSE`, over several axes at once too; `cumsum` is NA from the first gap on |
|
|
133
135
|
| `argmax`, `argmin` | the position of the first gap, numpy's rule for NaN, so `a[a.argmax()]` is NA exactly when `a.max()` is |
|
|
@@ -241,8 +243,8 @@ plain arrays, is in `NUMPY-PATCHES.md`.
|
|
|
241
243
|
widening to `int64`.
|
|
242
244
|
3. **A real `longdouble`** through `numpy-quaddtype` instead of substituting
|
|
243
245
|
`float64`.
|
|
244
|
-
4. **
|
|
245
|
-
visible on small arrays.
|
|
246
|
+
4. **Cache the loop lookups** — about 1.4 µs of the ~2.5 µs fixed cost per
|
|
247
|
+
operation, most visible on small arrays.
|
|
246
248
|
|
|
247
249
|
## Settled design notes
|
|
248
250
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nulldtype"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1"
|
|
8
8
|
description = "Missing values as a NumPy DType: NA stored as a reserved bit pattern of the wrapped type"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
nulldtype-0.1.1/setup.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""The C extension; everything else is in pyproject.toml."""
|
|
2
|
+
import platform
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from setuptools import Extension, setup
|
|
6
|
+
from setuptools.command.build_ext import build_ext
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BuildExt(build_ext):
|
|
10
|
+
"""numpy >= 2.5 already requires x86-64-v2 (SSE4.2) on x86-64, so the NA
|
|
11
|
+
scans may use it: SSE2 has no 64-bit compare, and they vectorise badly
|
|
12
|
+
without one. GCC and Clang only; MSVC and other CPUs keep their default."""
|
|
13
|
+
|
|
14
|
+
def build_extensions(self):
|
|
15
|
+
x86_64 = platform.machine().lower() in ("x86_64", "amd64")
|
|
16
|
+
if x86_64 and self.compiler.compiler_type != "msvc":
|
|
17
|
+
for ext in self.extensions:
|
|
18
|
+
ext.extra_compile_args += ["-march=x86-64-v2", "-mtune=generic"]
|
|
19
|
+
super().build_extensions()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
setup(
|
|
23
|
+
ext_modules=[
|
|
24
|
+
Extension(
|
|
25
|
+
"_nulldtype",
|
|
26
|
+
sources=["src/nulldtype.c"],
|
|
27
|
+
include_dirs=[np.get_include()],
|
|
28
|
+
),
|
|
29
|
+
],
|
|
30
|
+
cmdclass={"build_ext": BuildExt},
|
|
31
|
+
)
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
#include "numpy/ufuncobject.h"
|
|
39
39
|
#include "numpy/dtype_api.h"
|
|
40
40
|
|
|
41
|
+
/* MSVC's C mode spells C99 `restrict` as `__restrict` */
|
|
42
|
+
#if defined(_MSC_VER) && !defined(__cplusplus) && !defined(restrict)
|
|
43
|
+
#define restrict __restrict
|
|
44
|
+
#endif
|
|
45
|
+
|
|
41
46
|
static PyArray_DTypeMeta NullableDType;
|
|
42
47
|
|
|
43
48
|
/* the plain dtypes a Nullable one is the common dtype of */
|
|
@@ -642,22 +647,80 @@ typedef struct {
|
|
|
642
647
|
* failure rather than as a stray warning in someone's output.
|
|
643
648
|
*/
|
|
644
649
|
static int
|
|
645
|
-
binop_signals_on_gap(int idx)
|
|
650
|
+
binop_signals_on_gap(int idx, int complex_operand)
|
|
646
651
|
{
|
|
647
652
|
const char *n = binop_names[idx];
|
|
648
|
-
|
|
653
|
+
if (strcmp(n, "logaddexp") == 0 || strcmp(n, "logaddexp2") == 0) {
|
|
654
|
+
return 1;
|
|
655
|
+
}
|
|
656
|
+
/*
|
|
657
|
+
* The division family raises on a gap in several ways: an integer gap is
|
|
658
|
+
* INT_MIN, so `NA // -1` overflows, and `NA // 0` divides by zero for a
|
|
659
|
+
* value that is not there; MSVC's float32 `floor_divide` and `fmod`
|
|
660
|
+
* signal "invalid" on inf and NaN operands. A real zero divisor still
|
|
661
|
+
* warns, since the masked path computes every element that has a value.
|
|
662
|
+
*/
|
|
663
|
+
if (strcmp(n, "floor_divide") == 0 || strcmp(n, "remainder") == 0
|
|
664
|
+
|| strcmp(n, "fmod") == 0) {
|
|
665
|
+
return 1;
|
|
666
|
+
}
|
|
667
|
+
/*
|
|
668
|
+
* Complex kernels compare magnitudes almost everywhere: division scales by
|
|
669
|
+
* the larger component, `power` special-cases, ordering is lexicographic
|
|
670
|
+
* with `<`. Only the plain componentwise ops stay quiet over a NaN.
|
|
671
|
+
*/
|
|
672
|
+
if (complex_operand) {
|
|
673
|
+
return !(strcmp(n, "add") == 0 || strcmp(n, "subtract") == 0
|
|
674
|
+
|| strcmp(n, "multiply") == 0 || strcmp(n, "equal") == 0
|
|
675
|
+
|| strcmp(n, "not_equal") == 0);
|
|
676
|
+
}
|
|
677
|
+
return 0;
|
|
649
678
|
}
|
|
650
679
|
|
|
651
680
|
/*
|
|
652
|
-
* Ops
|
|
653
|
-
*
|
|
654
|
-
*
|
|
681
|
+
* Ops whose result can be a number although an operand is NaN, so a float gap
|
|
682
|
+
* can leave no NaN in the output: `fmax`/`fmin` return the other operand,
|
|
683
|
+
* `copysign` reads only the sign, and the ones in `binop_keeps_determined`.
|
|
684
|
+
* The fast path may skip a float block whose output holds no NaN only for the
|
|
685
|
+
* other ops. `test_every_binop_keeps_a_gap` fails if one is missing here.
|
|
655
686
|
*/
|
|
687
|
+
static int binop_keeps_determined(int idx);
|
|
688
|
+
|
|
656
689
|
static int
|
|
657
690
|
binop_swallows_nan(int idx)
|
|
658
691
|
{
|
|
659
692
|
const char *n = binop_names[idx];
|
|
660
|
-
return strcmp(n, "fmax") == 0 || strcmp(n, "fmin") == 0
|
|
693
|
+
return strcmp(n, "fmax") == 0 || strcmp(n, "fmin") == 0
|
|
694
|
+
|| strcmp(n, "copysign") == 0 || binop_keeps_determined(idx);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/*
|
|
698
|
+
* Ops whose IEEE 754 kernel already answers "does the result depend on the
|
|
699
|
+
* NaN operand?": it returns a number exactly when it does not. A gap is a
|
|
700
|
+
* value nobody knows, so the same rule applies to it, as in R and pandas:
|
|
701
|
+
*
|
|
702
|
+
* power, float_power 1 ** NA == 1, NA ** 0 == 1
|
|
703
|
+
* heaviside heaviside(x, NA) is 0 or 1 when x != 0
|
|
704
|
+
* hypot hypot(±inf, NA) == inf
|
|
705
|
+
*
|
|
706
|
+
* For these a float gap is stamped only where the output came out NaN. Other
|
|
707
|
+
* ops that turn a NaN into a number (`fmax`, `fmin`, `copysign`) do depend on
|
|
708
|
+
* the missing value, so they are not listed and always give NA. Integer `power` gets the same two rules
|
|
709
|
+
* in `nullable_int_power_rules`.
|
|
710
|
+
*/
|
|
711
|
+
static int
|
|
712
|
+
binop_keeps_determined(int idx)
|
|
713
|
+
{
|
|
714
|
+
const char *n = binop_names[idx];
|
|
715
|
+
return strcmp(n, "power") == 0 || strcmp(n, "float_power") == 0
|
|
716
|
+
|| strcmp(n, "heaviside") == 0 || strcmp(n, "hypot") == 0;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
static int
|
|
720
|
+
binop_is_power(int idx)
|
|
721
|
+
{
|
|
722
|
+
const char *n = binop_names[idx];
|
|
723
|
+
return strcmp(n, "power") == 0 || strcmp(n, "float_power") == 0;
|
|
661
724
|
}
|
|
662
725
|
|
|
663
726
|
/*
|
|
@@ -1833,114 +1896,42 @@ nullable_computed_na(const NullableDescr *out)
|
|
|
1833
1896
|
}
|
|
1834
1897
|
|
|
1835
1898
|
|
|
1836
|
-
#define
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
} \
|
|
1848
|
-
else if (CHECK) { \
|
|
1849
|
-
made |= (po[i] == na); \
|
|
1850
|
-
} \
|
|
1851
|
-
} \
|
|
1852
|
-
} while (0)
|
|
1899
|
+
#define ISNA_F8(v) (((v) & 0x7FFFFFFFFFFFFFFFULL) == 0x7FFFFFFFFFFFFFFFULL)
|
|
1900
|
+
#define ISNA_F4(v) (((v) & 0x7FFFFFFFU) == 0x7FFFFFFFU)
|
|
1901
|
+
#define ISNA_EQ(v) ((v) == na)
|
|
1902
|
+
/* NaN: exponent all ones, mantissa not zero */
|
|
1903
|
+
#define ISNAN_F8(v) ((((v) & 0x7FF0000000000000ULL) == 0x7FF0000000000000ULL) \
|
|
1904
|
+
& (((v) & 0x000FFFFFFFFFFFFFULL) != 0))
|
|
1905
|
+
#define ISNAN_F4(v) ((((v) & 0x7F800000U) == 0x7F800000U) & (((v) & 0x007FFFFFU) != 0))
|
|
1906
|
+
/* where may a gap be stamped: everywhere, or only where the op gave a NaN */
|
|
1907
|
+
#define STAMP_F8(v) ((!keep_numbers) | ISNAN_F8(v))
|
|
1908
|
+
#define STAMP_F4(v) ((!keep_numbers) | ISNAN_F4(v))
|
|
1909
|
+
#define STAMP_ANY(v) 1
|
|
1853
1910
|
|
|
1911
|
+
/* the output cell is a NaN; kinds without a NaN say yes, so they are stamped */
|
|
1854
1912
|
static int
|
|
1855
|
-
|
|
1856
|
-
char *const data[], npy_intp const strides[], npy_intp N,
|
|
1857
|
-
int propagates_nan)
|
|
1913
|
+
nullable_out_is_nan(const NullableDescr *out, const char *p)
|
|
1858
1914
|
{
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
int made = 0;
|
|
1862
|
-
int packed = (strides[0] == in0->base.elsize)
|
|
1863
|
-
&& (strides[1] == in1->base.elsize)
|
|
1864
|
-
&& (strides[2] == item)
|
|
1865
|
-
&& in0->kind == out->kind && in1->kind == out->kind;
|
|
1866
|
-
|
|
1867
|
-
if (packed) {
|
|
1868
|
-
/*
|
|
1869
|
-
* For floats the hardware has already put a NaN everywhere an input
|
|
1870
|
-
* was NA, so a single pass over the freshly written output — still hot
|
|
1871
|
-
* in cache — tells us whether there is anything to fix at all. Most
|
|
1872
|
-
* arrays have no gaps and stop here.
|
|
1873
|
-
*
|
|
1874
|
-
* This rests on the op propagating NaN. `fmax`/`fmin` do not: they
|
|
1875
|
-
* return the non-NaN side on purpose, so a gap leaves no trace in the
|
|
1876
|
-
* output and this scan would skip a fixup that is needed. They are
|
|
1877
|
-
* flagged by `binop_swallows_nan`, which turns this scan off and
|
|
1878
|
-
* forces the full pass; anything else added to `binop_names` that
|
|
1879
|
-
* swallows NaN must be listed there too.
|
|
1880
|
-
*/
|
|
1881
|
-
if (propagates_nan && out->kind == NA_KIND_F8) {
|
|
1882
|
-
const double *po = (const double *)data[2];
|
|
1883
|
-
int any = 0;
|
|
1884
|
-
for (npy_intp i = 0; i < N; i++) {
|
|
1885
|
-
any |= (po[i] != po[i]);
|
|
1886
|
-
}
|
|
1887
|
-
if (!any) {
|
|
1888
|
-
return 0;
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
else if (propagates_nan && out->kind == NA_KIND_F4) {
|
|
1892
|
-
const float *po = (const float *)data[2];
|
|
1893
|
-
int any = 0;
|
|
1894
|
-
for (npy_intp i = 0; i < N; i++) {
|
|
1895
|
-
any |= (po[i] != po[i]);
|
|
1896
|
-
}
|
|
1897
|
-
if (!any) {
|
|
1898
|
-
return 0;
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
switch (out->kind) {
|
|
1902
|
-
case NA_KIND_F8:
|
|
1903
|
-
NA_FIXUP(npy_uint64,
|
|
1904
|
-
((x & 0x7FFFFFFFFFFFFFFFULL) == 0x7FFFFFFFFFFFFFFFULL) ||
|
|
1905
|
-
((y & 0x7FFFFFFFFFFFFFFFULL) == 0x7FFFFFFFFFFFFFFFULL), 0);
|
|
1906
|
-
return 0;
|
|
1907
|
-
case NA_KIND_F4:
|
|
1908
|
-
NA_FIXUP(npy_uint32,
|
|
1909
|
-
((x & 0x7FFFFFFFU) == 0x7FFFFFFFU) ||
|
|
1910
|
-
((y & 0x7FFFFFFFU) == 0x7FFFFFFFU), 0);
|
|
1911
|
-
return 0;
|
|
1912
|
-
case NA_KIND_EXACT:
|
|
1913
|
-
if (item == 8) { NA_FIXUP(npy_uint64, x == na || y == na, 1); goto done; }
|
|
1914
|
-
if (item == 4) { NA_FIXUP(npy_uint32, x == na || y == na, 1); goto done; }
|
|
1915
|
-
if (item == 2) { NA_FIXUP(npy_uint16, x == na || y == na, 1); goto done; }
|
|
1916
|
-
if (item == 1) { NA_FIXUP(npy_uint8, x == na || y == na, 1); goto done; }
|
|
1917
|
-
break;
|
|
1918
|
-
default:
|
|
1919
|
-
/* f16 and complex: correctness first, via the loop below */
|
|
1920
|
-
break;
|
|
1921
|
-
}
|
|
1915
|
+
if (out->kind == NA_KIND_F8) {
|
|
1916
|
+
npy_uint64 v; memcpy(&v, p, 8); return ISNAN_F8(v);
|
|
1922
1917
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|| nullable_is_na(in1, data[1] + i * strides[1])) {
|
|
1926
|
-
nullable_put_na(out, data[2] + i * strides[2]);
|
|
1927
|
-
}
|
|
1928
|
-
else if (check && nullable_is_na(out, data[2] + i * strides[2])) {
|
|
1929
|
-
made = 1;
|
|
1930
|
-
}
|
|
1918
|
+
if (out->kind == NA_KIND_F4) {
|
|
1919
|
+
npy_uint32 v; memcpy(&v, p, 4); return ISNAN_F4(v);
|
|
1931
1920
|
}
|
|
1932
|
-
|
|
1933
|
-
|
|
1921
|
+
if (out->kind == NA_KIND_F2) {
|
|
1922
|
+
npy_uint16 v; memcpy(&v, p, 2);
|
|
1923
|
+
return ((v & 0x7C00U) == 0x7C00U) && ((v & 0x03FFU) != 0);
|
|
1924
|
+
}
|
|
1925
|
+
return 1;
|
|
1934
1926
|
}
|
|
1935
1927
|
|
|
1936
|
-
|
|
1937
1928
|
/* ------------------------------------------------------- ufunc loops */
|
|
1938
1929
|
/*
|
|
1939
1930
|
* The values are contiguous and aligned, so the wrapped loop runs at full
|
|
1940
|
-
* speed
|
|
1941
|
-
*
|
|
1942
|
-
*
|
|
1943
|
-
*
|
|
1931
|
+
* speed. The gaps are recorded before it runs and stamped after, so
|
|
1932
|
+
* `nan + NA` comes out as NA regardless of operand order, `a += 1` keeps its
|
|
1933
|
+
* gaps, and nothing relies on the CPU carrying a NaN payload through the
|
|
1934
|
+
* arithmetic or on an op propagating NaN at all.
|
|
1944
1935
|
*/
|
|
1945
1936
|
|
|
1946
1937
|
/*
|
|
@@ -2163,6 +2154,326 @@ nullable_binary_resolve_impl(PyObject *ufunc,
|
|
|
2163
2154
|
}
|
|
2164
2155
|
|
|
2165
2156
|
|
|
2157
|
+
/*
|
|
2158
|
+
* Gaps are recorded *before* the wrapped loop runs. Reading the inputs
|
|
2159
|
+
* afterwards is wrong whenever the output is one of them (`a += 1`,
|
|
2160
|
+
* `np.square(a, out=a)`): the loop has already overwritten the gap with
|
|
2161
|
+
* INT_MIN + 1 or 0, and the NA was lost to an ordinary number.
|
|
2162
|
+
*
|
|
2163
|
+
* The work goes in blocks small enough for L1, so each block is scanned,
|
|
2164
|
+
* computed and stamped while it is hot: one trip through memory instead of
|
|
2165
|
+
* one for the wrapped loop and another for the fixup. The typed scans are
|
|
2166
|
+
* branch-free integer tests, which the compiler vectorises at -O2.
|
|
2167
|
+
*/
|
|
2168
|
+
#define FIX_BLOCK 1024
|
|
2169
|
+
|
|
2170
|
+
/*
|
|
2171
|
+
* One small function per width, with `restrict` parameters: `mask` is a char
|
|
2172
|
+
* pointer, which may alias anything, and GCC at -O2 will not add a runtime
|
|
2173
|
+
* alias check, so without these it vectorises none of this. The two inputs
|
|
2174
|
+
* may be the same array (`a + a`); `restrict` allows that because neither is
|
|
2175
|
+
* written through.
|
|
2176
|
+
*/
|
|
2177
|
+
#define DEFINE_SCAN(NAME, CTYPE, ISNA) \
|
|
2178
|
+
static unsigned char \
|
|
2179
|
+
NAME(const CTYPE *restrict p0, const CTYPE *restrict p1, \
|
|
2180
|
+
unsigned char *restrict mk, npy_intp k, CTYPE na) \
|
|
2181
|
+
{ \
|
|
2182
|
+
unsigned char gap = 0; \
|
|
2183
|
+
(void)na; \
|
|
2184
|
+
for (npy_intp i = 0; i < k; i++) { \
|
|
2185
|
+
CTYPE x = p0[i], y = p1[i]; \
|
|
2186
|
+
unsigned char m = (unsigned char)(ISNA(x) | ISNA(y)); \
|
|
2187
|
+
mk[i] = m; \
|
|
2188
|
+
gap |= m; \
|
|
2189
|
+
} \
|
|
2190
|
+
return gap; \
|
|
2191
|
+
}
|
|
2192
|
+
#define DEFINE_SCAN1(NAME, CTYPE, ISNA) \
|
|
2193
|
+
static unsigned char \
|
|
2194
|
+
NAME(const CTYPE *restrict p0, unsigned char *restrict mk, npy_intp k, \
|
|
2195
|
+
CTYPE na) \
|
|
2196
|
+
{ \
|
|
2197
|
+
unsigned char gap = 0; \
|
|
2198
|
+
(void)na; \
|
|
2199
|
+
for (npy_intp i = 0; i < k; i++) { \
|
|
2200
|
+
CTYPE x = p0[i]; \
|
|
2201
|
+
unsigned char m = (unsigned char)ISNA(x); \
|
|
2202
|
+
mk[i] = m; \
|
|
2203
|
+
gap |= m; \
|
|
2204
|
+
} \
|
|
2205
|
+
return gap; \
|
|
2206
|
+
}
|
|
2207
|
+
DEFINE_SCAN1(scan1_f8, npy_uint64, ISNA_F8)
|
|
2208
|
+
DEFINE_SCAN1(scan1_f4, npy_uint32, ISNA_F4)
|
|
2209
|
+
DEFINE_SCAN1(scan1_u64, npy_uint64, ISNA_EQ)
|
|
2210
|
+
DEFINE_SCAN1(scan1_u32, npy_uint32, ISNA_EQ)
|
|
2211
|
+
DEFINE_SCAN1(scan1_u16, npy_uint16, ISNA_EQ)
|
|
2212
|
+
DEFINE_SCAN1(scan1_u8, npy_uint8, ISNA_EQ)
|
|
2213
|
+
#undef DEFINE_SCAN1
|
|
2214
|
+
|
|
2215
|
+
/* any NaN or inf (exponent all ones) in a float output block */
|
|
2216
|
+
static unsigned char
|
|
2217
|
+
special_f8(const npy_uint64 *restrict po, npy_intp k)
|
|
2218
|
+
{
|
|
2219
|
+
unsigned char any = 0;
|
|
2220
|
+
for (npy_intp i = 0; i < k; i++) {
|
|
2221
|
+
any |= (unsigned char)((po[i] & 0x7FF0000000000000ULL)
|
|
2222
|
+
== 0x7FF0000000000000ULL);
|
|
2223
|
+
}
|
|
2224
|
+
return any;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
static unsigned char
|
|
2228
|
+
special_f4(const npy_uint32 *restrict po, npy_intp k)
|
|
2229
|
+
{
|
|
2230
|
+
unsigned char any = 0;
|
|
2231
|
+
for (npy_intp i = 0; i < k; i++) {
|
|
2232
|
+
any |= (unsigned char)((po[i] & 0x7F800000U) == 0x7F800000U);
|
|
2233
|
+
}
|
|
2234
|
+
return any;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
DEFINE_SCAN(scan_f8, npy_uint64, ISNA_F8)
|
|
2238
|
+
DEFINE_SCAN(scan_f4, npy_uint32, ISNA_F4)
|
|
2239
|
+
DEFINE_SCAN(scan_u64, npy_uint64, ISNA_EQ)
|
|
2240
|
+
DEFINE_SCAN(scan_u32, npy_uint32, ISNA_EQ)
|
|
2241
|
+
DEFINE_SCAN(scan_u16, npy_uint16, ISNA_EQ)
|
|
2242
|
+
DEFINE_SCAN(scan_u8, npy_uint8, ISNA_EQ)
|
|
2243
|
+
#undef DEFINE_SCAN
|
|
2244
|
+
|
|
2245
|
+
/* the reserved pattern of `D` as a `CTYPE`; needs a local `na_<CTYPE>` */
|
|
2246
|
+
#define NA_OF(CTYPE, D) (memcpy(&na_##CTYPE, (D)->na_bytes, sizeof(CTYPE)), na_##CTYPE)
|
|
2247
|
+
|
|
2248
|
+
/* mask[i] = "an input of element i is a gap"; returns whether any is */
|
|
2249
|
+
static int
|
|
2250
|
+
nullable_scan_pair(const NullableDescr *in0, const NullableDescr *in1,
|
|
2251
|
+
char *const d[], npy_intp const strides[], npy_intp k,
|
|
2252
|
+
unsigned char *mask)
|
|
2253
|
+
{
|
|
2254
|
+
npy_intp item = in0->base.elsize;
|
|
2255
|
+
int packed = strides[0] == item && strides[1] == item
|
|
2256
|
+
&& in1->base.elsize == item && in0->kind == in1->kind
|
|
2257
|
+
&& memcmp(in0->na_bytes, in1->na_bytes, item) == 0;
|
|
2258
|
+
/* `a + a`, and the unary loops, pass one array twice: read it once */
|
|
2259
|
+
if (packed && d[0] == d[1]) {
|
|
2260
|
+
npy_uint64 na_npy_uint64;
|
|
2261
|
+
npy_uint32 na_npy_uint32;
|
|
2262
|
+
npy_uint16 na_npy_uint16;
|
|
2263
|
+
npy_uint8 na_npy_uint8;
|
|
2264
|
+
switch (in0->kind) {
|
|
2265
|
+
case NA_KIND_F8:
|
|
2266
|
+
return scan1_f8((npy_uint64 *)d[0], mask, k, 0);
|
|
2267
|
+
case NA_KIND_F4:
|
|
2268
|
+
return scan1_f4((npy_uint32 *)d[0], mask, k, 0);
|
|
2269
|
+
case NA_KIND_EXACT:
|
|
2270
|
+
if (item == 8) {
|
|
2271
|
+
return scan1_u64((npy_uint64 *)d[0], mask, k,
|
|
2272
|
+
NA_OF(npy_uint64, in0));
|
|
2273
|
+
}
|
|
2274
|
+
if (item == 4) {
|
|
2275
|
+
return scan1_u32((npy_uint32 *)d[0], mask, k,
|
|
2276
|
+
NA_OF(npy_uint32, in0));
|
|
2277
|
+
}
|
|
2278
|
+
if (item == 2) {
|
|
2279
|
+
return scan1_u16((npy_uint16 *)d[0], mask, k,
|
|
2280
|
+
NA_OF(npy_uint16, in0));
|
|
2281
|
+
}
|
|
2282
|
+
if (item == 1) {
|
|
2283
|
+
return scan1_u8((npy_uint8 *)d[0], mask, k,
|
|
2284
|
+
NA_OF(npy_uint8, in0));
|
|
2285
|
+
}
|
|
2286
|
+
break;
|
|
2287
|
+
default:
|
|
2288
|
+
break;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
if (packed) {
|
|
2292
|
+
npy_uint64 na_npy_uint64;
|
|
2293
|
+
npy_uint32 na_npy_uint32;
|
|
2294
|
+
npy_uint16 na_npy_uint16;
|
|
2295
|
+
npy_uint8 na_npy_uint8;
|
|
2296
|
+
switch (in0->kind) {
|
|
2297
|
+
case NA_KIND_F8:
|
|
2298
|
+
return scan_f8((npy_uint64 *)d[0], (npy_uint64 *)d[1], mask, k, 0);
|
|
2299
|
+
case NA_KIND_F4:
|
|
2300
|
+
return scan_f4((npy_uint32 *)d[0], (npy_uint32 *)d[1], mask, k, 0);
|
|
2301
|
+
case NA_KIND_EXACT:
|
|
2302
|
+
if (item == 8) {
|
|
2303
|
+
return scan_u64((npy_uint64 *)d[0], (npy_uint64 *)d[1], mask,
|
|
2304
|
+
k, NA_OF(npy_uint64, in0));
|
|
2305
|
+
}
|
|
2306
|
+
if (item == 4) {
|
|
2307
|
+
return scan_u32((npy_uint32 *)d[0], (npy_uint32 *)d[1], mask,
|
|
2308
|
+
k, NA_OF(npy_uint32, in0));
|
|
2309
|
+
}
|
|
2310
|
+
if (item == 2) {
|
|
2311
|
+
return scan_u16((npy_uint16 *)d[0], (npy_uint16 *)d[1], mask,
|
|
2312
|
+
k, NA_OF(npy_uint16, in0));
|
|
2313
|
+
}
|
|
2314
|
+
if (item == 1) {
|
|
2315
|
+
return scan_u8((npy_uint8 *)d[0], (npy_uint8 *)d[1], mask,
|
|
2316
|
+
k, NA_OF(npy_uint8, in0));
|
|
2317
|
+
}
|
|
2318
|
+
break;
|
|
2319
|
+
default:
|
|
2320
|
+
break;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
unsigned char gap = 0;
|
|
2324
|
+
for (npy_intp i = 0; i < k; i++) {
|
|
2325
|
+
mask[i] = (unsigned char)(nullable_is_na(in0, d[0] + i * strides[0])
|
|
2326
|
+
| nullable_is_na(in1, d[1] + i * strides[1]));
|
|
2327
|
+
gap |= mask[i];
|
|
2328
|
+
}
|
|
2329
|
+
return gap;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/*
|
|
2333
|
+
* The output side: returns 1 if an element without a gap landed on the
|
|
2334
|
+
* reserved value (only looked for when `check`), and stamps NA where `mk`
|
|
2335
|
+
* says -- for floats with `keep_numbers`, only where the op gave a NaN.
|
|
2336
|
+
*/
|
|
2337
|
+
#define DEFINE_STAMP(NAME, CTYPE, STAMP) \
|
|
2338
|
+
static unsigned char \
|
|
2339
|
+
NAME(CTYPE *restrict po, const unsigned char *restrict mk, npy_intp k, \
|
|
2340
|
+
CTYPE na, int check, int gap, int keep_numbers) \
|
|
2341
|
+
{ \
|
|
2342
|
+
(void)keep_numbers; \
|
|
2343
|
+
if (check) { \
|
|
2344
|
+
unsigned char hit = 0; \
|
|
2345
|
+
for (npy_intp i = 0; i < k; i++) { \
|
|
2346
|
+
hit |= (unsigned char)(!mk[i] & (po[i] == na)); \
|
|
2347
|
+
} \
|
|
2348
|
+
if (hit) { \
|
|
2349
|
+
return 1; \
|
|
2350
|
+
} \
|
|
2351
|
+
} \
|
|
2352
|
+
if (gap) { \
|
|
2353
|
+
for (npy_intp i = 0; i < k; i++) { \
|
|
2354
|
+
CTYPE v = po[i]; \
|
|
2355
|
+
unsigned char m = (unsigned char)(mk[i] & STAMP(v)); \
|
|
2356
|
+
po[i] = m ? na : v; \
|
|
2357
|
+
} \
|
|
2358
|
+
} \
|
|
2359
|
+
return 0; \
|
|
2360
|
+
}
|
|
2361
|
+
DEFINE_STAMP(stamp_f8, npy_uint64, STAMP_F8)
|
|
2362
|
+
DEFINE_STAMP(stamp_f4, npy_uint32, STAMP_F4)
|
|
2363
|
+
DEFINE_STAMP(stamp_u64, npy_uint64, STAMP_ANY)
|
|
2364
|
+
DEFINE_STAMP(stamp_u32, npy_uint32, STAMP_ANY)
|
|
2365
|
+
DEFINE_STAMP(stamp_u16, npy_uint16, STAMP_ANY)
|
|
2366
|
+
DEFINE_STAMP(stamp_u8, npy_uint8, STAMP_ANY)
|
|
2367
|
+
#undef DEFINE_STAMP
|
|
2368
|
+
|
|
2369
|
+
/*
|
|
2370
|
+
* Put NA where `mask` says, and refuse a result that landed on the reserved
|
|
2371
|
+
* value elsewhere. With `keep_numbers` a float gap is stamped only where the
|
|
2372
|
+
* op gave a NaN (`binop_keeps_determined`).
|
|
2373
|
+
*/
|
|
2374
|
+
static int
|
|
2375
|
+
nullable_stamp(const NullableDescr *out, char *o, npy_intp stride, npy_intp k,
|
|
2376
|
+
const unsigned char *mask, int gap, int keep_numbers)
|
|
2377
|
+
{
|
|
2378
|
+
int check = na_can_be_computed(out->kind);
|
|
2379
|
+
if (!gap && !check) {
|
|
2380
|
+
return 0;
|
|
2381
|
+
}
|
|
2382
|
+
npy_intp item = out->base.elsize;
|
|
2383
|
+
if (stride == item) {
|
|
2384
|
+
npy_uint64 na_npy_uint64;
|
|
2385
|
+
npy_uint32 na_npy_uint32;
|
|
2386
|
+
npy_uint16 na_npy_uint16;
|
|
2387
|
+
npy_uint8 na_npy_uint8;
|
|
2388
|
+
int hit = -1;
|
|
2389
|
+
switch (out->kind) {
|
|
2390
|
+
case NA_KIND_F8:
|
|
2391
|
+
hit = stamp_f8((npy_uint64 *)o, mask, k, NA_OF(npy_uint64, out),
|
|
2392
|
+
0, gap, keep_numbers);
|
|
2393
|
+
break;
|
|
2394
|
+
case NA_KIND_F4:
|
|
2395
|
+
hit = stamp_f4((npy_uint32 *)o, mask, k, NA_OF(npy_uint32, out),
|
|
2396
|
+
0, gap, keep_numbers);
|
|
2397
|
+
break;
|
|
2398
|
+
case NA_KIND_EXACT:
|
|
2399
|
+
if (item == 8) {
|
|
2400
|
+
hit = stamp_u64((npy_uint64 *)o, mask, k,
|
|
2401
|
+
NA_OF(npy_uint64, out), check, gap, 0);
|
|
2402
|
+
}
|
|
2403
|
+
else if (item == 4) {
|
|
2404
|
+
hit = stamp_u32((npy_uint32 *)o, mask, k,
|
|
2405
|
+
NA_OF(npy_uint32, out), check, gap, 0);
|
|
2406
|
+
}
|
|
2407
|
+
else if (item == 2) {
|
|
2408
|
+
hit = stamp_u16((npy_uint16 *)o, mask, k,
|
|
2409
|
+
NA_OF(npy_uint16, out), check, gap, 0);
|
|
2410
|
+
}
|
|
2411
|
+
else if (item == 1) {
|
|
2412
|
+
hit = stamp_u8((npy_uint8 *)o, mask, k,
|
|
2413
|
+
NA_OF(npy_uint8, out), check, gap, 0);
|
|
2414
|
+
}
|
|
2415
|
+
break;
|
|
2416
|
+
default:
|
|
2417
|
+
break;
|
|
2418
|
+
}
|
|
2419
|
+
if (hit >= 0) {
|
|
2420
|
+
return hit ? nullable_computed_na(out) : 0;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
for (npy_intp i = 0; i < k; i++) {
|
|
2424
|
+
char *po = o + i * stride;
|
|
2425
|
+
if (mask[i]) {
|
|
2426
|
+
if (!keep_numbers || nullable_out_is_nan(out, po)) {
|
|
2427
|
+
nullable_put_na(out, po);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
else if (check && nullable_is_na(out, po)) {
|
|
2431
|
+
return nullable_computed_na(out);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
return 0;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
|
|
2438
|
+
/*
|
|
2439
|
+
* Integer `power` over a gap: `1 ** NA` and `NA ** 0` are 1 whatever the gap
|
|
2440
|
+
* holds (see `binop_keeps_determined`). The masked path has already put NA in
|
|
2441
|
+
* every gap lane; this puts the 1 back where it is determined.
|
|
2442
|
+
*/
|
|
2443
|
+
static int
|
|
2444
|
+
nullable_int_power_rules(NullableDescr *in0, NullableDescr *in1,
|
|
2445
|
+
NullableDescr *out, char *const data[], npy_intp const strides[],
|
|
2446
|
+
npy_intp N)
|
|
2447
|
+
{
|
|
2448
|
+
char one0[16] = {0}, zero1[16] = {0}, one_out[16] = {0};
|
|
2449
|
+
if (in0->wrapped->elsize > 16 || in1->wrapped->elsize > 16
|
|
2450
|
+
|| out->wrapped->elsize > 16) {
|
|
2451
|
+
return 0;
|
|
2452
|
+
}
|
|
2453
|
+
PyObject *one = PyLong_FromLong(1);
|
|
2454
|
+
PyObject *zero = PyLong_FromLong(0);
|
|
2455
|
+
int rc = (one && zero
|
|
2456
|
+
&& PyArray_Pack(in0->wrapped, one0, one) >= 0
|
|
2457
|
+
&& PyArray_Pack(in1->wrapped, zero1, zero) >= 0
|
|
2458
|
+
&& PyArray_Pack(out->wrapped, one_out, one) >= 0) ? 0 : -1;
|
|
2459
|
+
Py_XDECREF(one);
|
|
2460
|
+
Py_XDECREF(zero);
|
|
2461
|
+
if (rc < 0) {
|
|
2462
|
+
return -1;
|
|
2463
|
+
}
|
|
2464
|
+
for (npy_intp i = 0; i < N; i++) {
|
|
2465
|
+
const char *x = data[0] + i * strides[0];
|
|
2466
|
+
const char *y = data[1] + i * strides[1];
|
|
2467
|
+
int xna = nullable_is_na(in0, x), yna = nullable_is_na(in1, y);
|
|
2468
|
+
if ((yna && !xna && memcmp(x, one0, in0->wrapped->elsize) == 0)
|
|
2469
|
+
|| (xna && !yna && memcmp(y, zero1, in1->wrapped->elsize) == 0)) {
|
|
2470
|
+
memcpy(data[2] + i * strides[2], one_out, out->wrapped->elsize);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
return 0;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
|
|
2166
2477
|
static int
|
|
2167
2478
|
nullable_binary_loop_impl(int idx, PyArrayMethod_Context *context,
|
|
2168
2479
|
char *const data[], npy_intp const dimensions[],
|
|
@@ -2204,10 +2515,10 @@ nullable_binary_loop_impl(int idx, PyArrayMethod_Context *context,
|
|
|
2204
2515
|
Py_DECREF(a); Py_DECREF(b); Py_DECREF(o);
|
|
2205
2516
|
return -1;
|
|
2206
2517
|
}
|
|
2207
|
-
char *mask = PyArray_DATA((PyArrayObject *)both);
|
|
2518
|
+
unsigned char *mask = (unsigned char *)PyArray_DATA((PyArrayObject *)both);
|
|
2519
|
+
int gap = nullable_scan_pair(in0, in1, data, strides, N, mask);
|
|
2208
2520
|
for (npy_intp i = 0; i < N; i++) {
|
|
2209
|
-
mask[i] = !
|
|
2210
|
-
&& !nullable_is_na(in1, data[1] + i * strides[1]);
|
|
2521
|
+
mask[i] = !mask[i]; /* `where=`: the valid ones */
|
|
2211
2522
|
}
|
|
2212
2523
|
|
|
2213
2524
|
PyObject *args = PyTuple_Pack(2, a, b);
|
|
@@ -2216,25 +2527,37 @@ nullable_binary_loop_impl(int idx, PyArrayMethod_Context *context,
|
|
|
2216
2527
|
? PyObject_Call(binop_ufuncs[idx], args, kwargs) : NULL;
|
|
2217
2528
|
Py_XDECREF(args); Py_XDECREF(kwargs);
|
|
2218
2529
|
Py_DECREF(a); Py_DECREF(b); Py_DECREF(o);
|
|
2219
|
-
Py_DECREF(both);
|
|
2220
2530
|
if (res == NULL) {
|
|
2531
|
+
Py_DECREF(both);
|
|
2221
2532
|
return -1;
|
|
2222
2533
|
}
|
|
2223
2534
|
Py_DECREF(res);
|
|
2224
2535
|
|
|
2225
2536
|
/*
|
|
2226
|
-
* `where=` left the gap lanes untouched, so
|
|
2227
|
-
*
|
|
2537
|
+
* `where=` left the gap lanes untouched, so they hold whatever the buffer
|
|
2538
|
+
* held before -- never a reliable NaN, so the "keep a determined number"
|
|
2539
|
+
* rule must not read them.
|
|
2228
2540
|
*/
|
|
2229
|
-
|
|
2541
|
+
for (npy_intp i = 0; i < N; i++) {
|
|
2542
|
+
mask[i] = !mask[i]; /* back to "is a gap" */
|
|
2543
|
+
}
|
|
2544
|
+
int rc = nullable_stamp(out, data[2], strides[2], N, mask, gap, 0);
|
|
2545
|
+
Py_DECREF(both);
|
|
2546
|
+
if (rc < 0) {
|
|
2547
|
+
return -1;
|
|
2548
|
+
}
|
|
2549
|
+
if (binop_is_power(idx) && out->kind == NA_KIND_EXACT) {
|
|
2550
|
+
return nullable_int_power_rules(in0, in1, out, data, strides, N);
|
|
2551
|
+
}
|
|
2552
|
+
return 0;
|
|
2230
2553
|
}
|
|
2231
2554
|
|
|
2232
2555
|
|
|
2233
2556
|
/*
|
|
2234
2557
|
* The fast path: values are contiguous and aligned, so the wrapped loop runs
|
|
2235
|
-
*
|
|
2236
|
-
*
|
|
2237
|
-
*
|
|
2558
|
+
* at native speed, gaps included -- a gap holds a NaN or INT_MIN, and computing
|
|
2559
|
+
* with those is harmless and quiet (`binop_signals_on_gap` lists the ops for
|
|
2560
|
+
* which it is not). See `nullable_scan_pair` and `nullable_stamp`.
|
|
2238
2561
|
*/
|
|
2239
2562
|
typedef struct {
|
|
2240
2563
|
NpyAuxData base;
|
|
@@ -2270,15 +2593,55 @@ nullable_fast_loop(PyArrayMethod_Context *context, char *const data[],
|
|
|
2270
2593
|
return nullable_accumulate_chunk(aux->idx, context, data, N, strides);
|
|
2271
2594
|
}
|
|
2272
2595
|
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2596
|
+
NullableDescr *in0 = NULLABLE_DESCR(context->descriptors[0]);
|
|
2597
|
+
NullableDescr *in1 = NULLABLE_DESCR(context->descriptors[1]);
|
|
2598
|
+
NullableDescr *out = NULLABLE_DESCR(context->descriptors[2]);
|
|
2599
|
+
int keep = binop_keeps_determined(aux->idx);
|
|
2600
|
+
unsigned char mask[FIX_BLOCK];
|
|
2601
|
+
/*
|
|
2602
|
+
* Floats that propagate NaN can do better than scanning both inputs up
|
|
2603
|
+
* front: compute, look at the output only (one stream), and scan the
|
|
2604
|
+
* inputs only in a block that turned up a NaN. That needs the inputs
|
|
2605
|
+
* intact afterwards, so not when the output is one of them.
|
|
2606
|
+
*/
|
|
2607
|
+
npy_intp item = out->base.elsize;
|
|
2608
|
+
int post = (out->kind == NA_KIND_F8 || out->kind == NA_KIND_F4)
|
|
2609
|
+
&& in0->kind == out->kind && in1->kind == out->kind
|
|
2610
|
+
&& strides[2] == item
|
|
2611
|
+
&& !binop_swallows_nan(aux->idx)
|
|
2612
|
+
&& data[2] != data[0] && data[2] != data[1];
|
|
2613
|
+
for (npy_intp s = 0; s < N; s += FIX_BLOCK) {
|
|
2614
|
+
npy_intp k = (N - s < FIX_BLOCK) ? N - s : FIX_BLOCK;
|
|
2615
|
+
char *d[3] = {data[0] + s * strides[0], data[1] + s * strides[1],
|
|
2616
|
+
data[2] + s * strides[2]};
|
|
2617
|
+
npy_intp count = k;
|
|
2618
|
+
if (post) {
|
|
2619
|
+
if (aux->inner(aux->inner_context, d, &count, strides,
|
|
2620
|
+
aux->inner_auxdata) < 0) {
|
|
2621
|
+
return -1;
|
|
2622
|
+
}
|
|
2623
|
+
unsigned char special = (out->kind == NA_KIND_F8)
|
|
2624
|
+
? special_f8((npy_uint64 *)d[2], k)
|
|
2625
|
+
: special_f4((npy_uint32 *)d[2], k);
|
|
2626
|
+
if (!special) {
|
|
2627
|
+
continue;
|
|
2628
|
+
}
|
|
2629
|
+
int gap = nullable_scan_pair(in0, in1, d, strides, k, mask);
|
|
2630
|
+
if (nullable_stamp(out, d[2], strides[2], k, mask, gap, keep) < 0) {
|
|
2631
|
+
return -1;
|
|
2632
|
+
}
|
|
2633
|
+
continue;
|
|
2634
|
+
}
|
|
2635
|
+
int gap = nullable_scan_pair(in0, in1, d, strides, k, mask);
|
|
2636
|
+
if (aux->inner(aux->inner_context, d, &count, strides,
|
|
2637
|
+
aux->inner_auxdata) < 0) {
|
|
2638
|
+
return -1;
|
|
2639
|
+
}
|
|
2640
|
+
if (nullable_stamp(out, d[2], strides[2], k, mask, gap, keep) < 0) {
|
|
2641
|
+
return -1;
|
|
2642
|
+
}
|
|
2277
2643
|
}
|
|
2278
|
-
return
|
|
2279
|
-
NULLABLE_DESCR(context->descriptors[1]),
|
|
2280
|
-
NULLABLE_DESCR(context->descriptors[2]),
|
|
2281
|
-
data, strides, N, !binop_swallows_nan(aux->idx));
|
|
2644
|
+
return 0;
|
|
2282
2645
|
}
|
|
2283
2646
|
|
|
2284
2647
|
|
|
@@ -2297,7 +2660,17 @@ nullable_get_loop_impl(int idx, PyArrayMethod_Context *context,
|
|
|
2297
2660
|
return 1;
|
|
2298
2661
|
}
|
|
2299
2662
|
/* these must not run over a gap at all; the masked path handles them */
|
|
2300
|
-
|
|
2663
|
+
int k0 = NULLABLE_DESCR(context->descriptors[0])->kind;
|
|
2664
|
+
int k1 = NULLABLE_DESCR(context->descriptors[1])->kind;
|
|
2665
|
+
int complex_operand = k0 == NA_KIND_C8 || k0 == NA_KIND_C16
|
|
2666
|
+
|| k1 == NA_KIND_C8 || k1 == NA_KIND_C16;
|
|
2667
|
+
/*
|
|
2668
|
+
* An integer gap is INT_MIN, which integer `power` reads as a negative
|
|
2669
|
+
* exponent and refuses; it must not see the gap at all.
|
|
2670
|
+
*/
|
|
2671
|
+
int int_power = binop_is_power(idx)
|
|
2672
|
+
&& (k0 == NA_KIND_EXACT || k1 == NA_KIND_EXACT);
|
|
2673
|
+
if (binop_signals_on_gap(idx, complex_operand) || int_power) {
|
|
2301
2674
|
*out_auxdata = NULL;
|
|
2302
2675
|
return 1;
|
|
2303
2676
|
}
|
|
@@ -2574,34 +2947,37 @@ nullable_unary_loop_impl(int idx, PyArrayMethod_Context *context,
|
|
|
2574
2947
|
NullableDescr *in = NULLABLE_DESCR(context->descriptors[0]);
|
|
2575
2948
|
NullableDescr *out = NULLABLE_DESCR(context->descriptors[1]);
|
|
2576
2949
|
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2950
|
+
/*
|
|
2951
|
+
* Record the gaps first: with `out=` the same array (`np.square(a,
|
|
2952
|
+
* out=a)`) the op overwrites them, and INT_MIN squared is 0.
|
|
2953
|
+
*/
|
|
2954
|
+
unsigned char *mask = PyMem_Malloc(N > 0 ? N : 1);
|
|
2955
|
+
if (mask == NULL) {
|
|
2956
|
+
PyErr_NoMemory();
|
|
2581
2957
|
return -1;
|
|
2582
2958
|
}
|
|
2583
|
-
|
|
2584
|
-
|
|
2959
|
+
char *const twice[2] = {data[0], data[0]};
|
|
2960
|
+
npy_intp const twice_strides[2] = {strides[0], strides[0]};
|
|
2961
|
+
int gap = nullable_scan_pair(in, in, twice, twice_strides, N, mask);
|
|
2962
|
+
|
|
2963
|
+
PyObject *a = value_view(in->wrapped, data[0], N, strides[0]);
|
|
2964
|
+
PyObject *o = value_view(out->wrapped, data[1], N, strides[1]);
|
|
2965
|
+
PyObject *args = (a && o) ? PyTuple_Pack(1, a) : NULL;
|
|
2966
|
+
PyObject *kwargs = (args != NULL) ? Py_BuildValue("{s:O}", "out", o) : NULL;
|
|
2585
2967
|
PyObject *res = (args && kwargs)
|
|
2586
2968
|
? PyObject_Call(unop_ufuncs[idx], args, kwargs) : NULL;
|
|
2587
2969
|
Py_XDECREF(args); Py_XDECREF(kwargs);
|
|
2588
|
-
|
|
2970
|
+
Py_XDECREF(a); Py_XDECREF(o);
|
|
2589
2971
|
if (res == NULL) {
|
|
2972
|
+
PyMem_Free(mask);
|
|
2590
2973
|
return -1;
|
|
2591
2974
|
}
|
|
2592
2975
|
Py_DECREF(res);
|
|
2593
2976
|
|
|
2594
2977
|
/* a unary op cannot create or remove a gap, it just carries it over */
|
|
2595
|
-
int
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
nullable_put_na(out, data[1] + i * strides[1]);
|
|
2599
|
-
}
|
|
2600
|
-
else if (check && nullable_is_na(out, data[1] + i * strides[1])) {
|
|
2601
|
-
return nullable_computed_na(out);
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
return 0;
|
|
2978
|
+
int rc = nullable_stamp(out, data[1], strides[1], N, mask, gap, 0);
|
|
2979
|
+
PyMem_Free(mask);
|
|
2980
|
+
return rc;
|
|
2605
2981
|
}
|
|
2606
2982
|
|
|
2607
2983
|
|
|
@@ -2419,3 +2419,225 @@ def test_numpy_mean_of_nullable_int_is_not_truncated():
|
|
|
2419
2419
|
i = np.array([1, 2], dtype=nd.Nullable(np.int64))
|
|
2420
2420
|
assert nd.mean(i) == 1.5 # the nd version is right
|
|
2421
2421
|
assert np.mean(i) == 1.5 # numpy's gives 1
|
|
2422
|
+
|
|
2423
|
+
|
|
2424
|
+
@pytest.mark.parametrize("t", [np.float64, np.float32, np.int64, np.int32,
|
|
2425
|
+
np.int16, np.uint8])
|
|
2426
|
+
def test_block_scan_edges(t):
|
|
2427
|
+
"""The fixup scans in blocks of 2048 and skips blocks with no gap; a gap on
|
|
2428
|
+
either side of a block edge, in the last short block, or in only one
|
|
2429
|
+
operand must still come out as NA, and nothing else may."""
|
|
2430
|
+
n = 2048 * 3 + 5
|
|
2431
|
+
dt = nd.Nullable(t)
|
|
2432
|
+
a = np.ones(n, dtype=dt)
|
|
2433
|
+
b = np.ones(n, dtype=dt)
|
|
2434
|
+
gaps_a = [0, 2047, 2048, 4095, n - 1]
|
|
2435
|
+
gaps_b = [100, 4096, n - 3]
|
|
2436
|
+
for i in gaps_a:
|
|
2437
|
+
a[i] = nd.NA
|
|
2438
|
+
for i in gaps_b:
|
|
2439
|
+
b[i] = nd.NA
|
|
2440
|
+
got = nd.isna(a + b)
|
|
2441
|
+
want = np.zeros(n, dtype=bool)
|
|
2442
|
+
want[gaps_a + gaps_b] = True
|
|
2443
|
+
np.testing.assert_array_equal(got, want)
|
|
2444
|
+
assert (nd.filled(a + b, 0)[~want] == 2).all()
|
|
2445
|
+
|
|
2446
|
+
|
|
2447
|
+
def test_block_scan_still_catches_a_computed_na_after_a_clean_block():
|
|
2448
|
+
"""A block without gaps still checks whether the result landed on the
|
|
2449
|
+
reserved value."""
|
|
2450
|
+
n = 2048 * 2 + 1
|
|
2451
|
+
a = np.zeros(n, dtype=nd.Nullable(np.int32))
|
|
2452
|
+
a[-1] = np.iinfo(np.int32).max
|
|
2453
|
+
with pytest.raises(ValueError, match="reserved value"):
|
|
2454
|
+
a + np.ones(n, dtype=nd.Nullable(np.int32))
|
|
2455
|
+
|
|
2456
|
+
|
|
2457
|
+
_SPECIALS = [0.0, -0.0, 1.0, -1.0, 2.0, 0.5, np.inf, -np.inf, np.nan]
|
|
2458
|
+
# ops whose answer depends on the missing value although IEEE gives a number
|
|
2459
|
+
_DEPENDS_ON_GAP = {"fmax", "fmin", "copysign"}
|
|
2460
|
+
|
|
2461
|
+
|
|
2462
|
+
@pytest.mark.parametrize("t", [np.float64, np.float32, np.float16,
|
|
2463
|
+
np.complex128, np.complex64])
|
|
2464
|
+
@pytest.mark.parametrize("n", [1, 5000])
|
|
2465
|
+
def test_every_binop_keeps_a_gap(t, n):
|
|
2466
|
+
"""A gap is a value nobody knows. Where the result would differ for some
|
|
2467
|
+
value of it, the result is NA; where IEEE 754 already says it cannot --
|
|
2468
|
+
`1 ** x == x ** 0 == 1`, `hypot(inf, x) == inf`, `heaviside(y != 0, x)` --
|
|
2469
|
+
the number stands, as in R and pandas. So the expected answer is IEEE's
|
|
2470
|
+
with a NaN in place of the gap, NA wherever that is NaN, and NA always for
|
|
2471
|
+
`fmax`, `fmin` and `copysign`, which do look at the missing value.
|
|
2472
|
+
Complex operands conservatively always give NA.
|
|
2473
|
+
|
|
2474
|
+
Every element involves a gap, so any floating-point warning is a spurious
|
|
2475
|
+
one (`binop_signals_on_gap`); complex division, power and ordering used to
|
|
2476
|
+
raise "invalid value" this way."""
|
|
2477
|
+
dt = nd.Nullable(t)
|
|
2478
|
+
is_complex = np.dtype(t).kind == "c"
|
|
2479
|
+
gap = np.empty(n, dtype=dt)
|
|
2480
|
+
gap[...] = nd.NA
|
|
2481
|
+
nan = np.full(n, np.nan, dtype=t)
|
|
2482
|
+
wrong = []
|
|
2483
|
+
for name in _c.binop_names:
|
|
2484
|
+
f = getattr(np, name)
|
|
2485
|
+
for v in _SPECIALS:
|
|
2486
|
+
plain = np.full(n, v, dtype=t)
|
|
2487
|
+
other = plain.astype(dt)
|
|
2488
|
+
for x, y, px, py, side in ((other, gap, plain, nan, "right"),
|
|
2489
|
+
(gap, other, nan, plain, "left")):
|
|
2490
|
+
try:
|
|
2491
|
+
with warnings.catch_warnings():
|
|
2492
|
+
warnings.simplefilter("error", RuntimeWarning)
|
|
2493
|
+
r = f(x, y)
|
|
2494
|
+
except RuntimeWarning as w:
|
|
2495
|
+
wrong.append((name, v, side, str(w)))
|
|
2496
|
+
continue
|
|
2497
|
+
except (TypeError, ValueError):
|
|
2498
|
+
continue # op not defined for this dtype
|
|
2499
|
+
if not nd.is_nullable(r.dtype):
|
|
2500
|
+
continue
|
|
2501
|
+
with np.errstate(all="ignore"):
|
|
2502
|
+
ref = np.asarray(f(px, py))
|
|
2503
|
+
if ref.dtype.kind in "fc" and not is_complex \
|
|
2504
|
+
and name not in _DEPENDS_ON_GAP:
|
|
2505
|
+
expect_na = np.isnan(ref)
|
|
2506
|
+
else:
|
|
2507
|
+
expect_na = np.ones(n, dtype=bool)
|
|
2508
|
+
got_na = nd.isna(r)
|
|
2509
|
+
if not (got_na == expect_na).all():
|
|
2510
|
+
wrong.append((name, v, side, "NA where", got_na[0], "expected", expect_na[0]))
|
|
2511
|
+
continue
|
|
2512
|
+
if (~expect_na).any():
|
|
2513
|
+
kept = nd.to_numpy(r[~expect_na])
|
|
2514
|
+
if not (kept == ref[~expect_na]).all():
|
|
2515
|
+
wrong.append((name, v, side, "value", kept[0], ref[~expect_na][0]))
|
|
2516
|
+
assert not wrong
|
|
2517
|
+
|
|
2518
|
+
|
|
2519
|
+
@pytest.mark.parametrize("t", [np.int64, np.int32, np.int8, np.uint8, np.uint64])
|
|
2520
|
+
@pytest.mark.parametrize("n", [1, 5000])
|
|
2521
|
+
def test_integer_power_over_a_gap(t, n):
|
|
2522
|
+
"""An integer gap is INT_MIN (or UINT_MAX); integer `power` used to read it
|
|
2523
|
+
as a negative exponent and raise. Now it never sees the gap, and the two
|
|
2524
|
+
determined cases give 1 as for floats. A real negative exponent still
|
|
2525
|
+
raises, as for a plain array."""
|
|
2526
|
+
dt = nd.Nullable(t)
|
|
2527
|
+
base = np.array([1, 2, 0, 0, 3], dtype=t).astype(dt)
|
|
2528
|
+
expo = np.array([0, 0, 0, 2, 0], dtype=t).astype(dt)
|
|
2529
|
+
base[2] = base[3] = nd.NA
|
|
2530
|
+
expo[0] = expo[1] = nd.NA
|
|
2531
|
+
base, expo = np.tile(base, n), np.tile(expo, n)
|
|
2532
|
+
got = np.power(base, expo)
|
|
2533
|
+
assert nd.isna(got).tolist() == [False, True, False, True, False] * n
|
|
2534
|
+
assert got[0] == 1 and got[2] == 1 and got[4] == 1
|
|
2535
|
+
|
|
2536
|
+
if np.dtype(t).kind == "i":
|
|
2537
|
+
with pytest.raises(ValueError, match="negative integer powers"):
|
|
2538
|
+
np.power(np.array([2], dtype=t).astype(dt),
|
|
2539
|
+
np.array([-1], dtype=t).astype(dt))
|
|
2540
|
+
|
|
2541
|
+
|
|
2542
|
+
_INPLACE_TYPES = [np.int8, np.int32, np.int64, np.uint8, np.uint64,
|
|
2543
|
+
np.float16, np.float32, np.float64, np.complex128]
|
|
2544
|
+
|
|
2545
|
+
|
|
2546
|
+
@pytest.mark.parametrize("t", _INPLACE_TYPES)
|
|
2547
|
+
@pytest.mark.parametrize("n", [3, 5000])
|
|
2548
|
+
def test_inplace_ops_keep_their_gaps(t, n):
|
|
2549
|
+
"""With `out=` one of the inputs, the wrapped loop overwrites the gap
|
|
2550
|
+
before anything looks at it: `a += 1` turned an int32 gap into
|
|
2551
|
+
-2147483647, `np.square(a, out=a)` into 0. Gaps are now recorded before
|
|
2552
|
+
the loop runs."""
|
|
2553
|
+
dt = nd.Nullable(t)
|
|
2554
|
+
|
|
2555
|
+
def with_gaps():
|
|
2556
|
+
a = np.full(n, 3, dtype=t).astype(dt)
|
|
2557
|
+
a[1] = a[n - 1] = nd.NA
|
|
2558
|
+
return a
|
|
2559
|
+
|
|
2560
|
+
gaps = np.zeros(n, dtype=bool)
|
|
2561
|
+
gaps[[1, n - 1]] = True
|
|
2562
|
+
ones = np.ones(n, dtype=t).astype(dt)
|
|
2563
|
+
|
|
2564
|
+
with warnings.catch_warnings():
|
|
2565
|
+
warnings.simplefilter("ignore", RuntimeWarning) # e.g. 3 ** 3 in int8
|
|
2566
|
+
for name in ("add", "subtract", "multiply", "maximum"):
|
|
2567
|
+
a = with_gaps()
|
|
2568
|
+
getattr(np, name)(a, ones, out=a)
|
|
2569
|
+
np.testing.assert_array_equal(nd.isna(a), gaps, err_msg=name)
|
|
2570
|
+
b = with_gaps()
|
|
2571
|
+
getattr(np, name)(ones, b, out=b)
|
|
2572
|
+
np.testing.assert_array_equal(nd.isna(b), gaps, err_msg=name)
|
|
2573
|
+
c = with_gaps()
|
|
2574
|
+
getattr(np, name)(c, c, out=c)
|
|
2575
|
+
np.testing.assert_array_equal(nd.isna(c), gaps, err_msg=name)
|
|
2576
|
+
if np.dtype(t).kind in "fc":
|
|
2577
|
+
# x86 keeps the left NaN's payload, so writing `nan + NA` over the
|
|
2578
|
+
# right operand replaces the gap with an ordinary NaN
|
|
2579
|
+
nan = np.full(n, np.nan, dtype=t).astype(dt)
|
|
2580
|
+
b = with_gaps()
|
|
2581
|
+
np.add(nan, b, out=b)
|
|
2582
|
+
np.testing.assert_array_equal(nd.isna(b), gaps, err_msg="nan + NA")
|
|
2583
|
+
for name in ("square", "negative", "absolute", "invert", "sign",
|
|
2584
|
+
"sqrt", "reciprocal"):
|
|
2585
|
+
a = with_gaps()
|
|
2586
|
+
try:
|
|
2587
|
+
getattr(np, name)(a, out=a)
|
|
2588
|
+
except (TypeError, ValueError):
|
|
2589
|
+
continue # not defined for this dtype, or unsafe out
|
|
2590
|
+
np.testing.assert_array_equal(nd.isna(a), gaps, err_msg=name)
|
|
2591
|
+
|
|
2592
|
+
|
|
2593
|
+
@pytest.mark.parametrize("t", [np.int8, np.int32, np.int64, np.uint8, np.uint64])
|
|
2594
|
+
@pytest.mark.parametrize("n", [1, 5000])
|
|
2595
|
+
def test_every_binop_keeps_a_gap_integers(t, n):
|
|
2596
|
+
"""The integer side of `test_every_binop_keeps_a_gap`: a gap is INT_MIN or
|
|
2597
|
+
UINT_MAX, so `NA // -1` overflowed and `NA // 0` divided by zero -- warnings
|
|
2598
|
+
about a value that is not there. Every result is NA except the two
|
|
2599
|
+
determined powers, and a real zero divisor still warns."""
|
|
2600
|
+
dt = nd.Nullable(t)
|
|
2601
|
+
gap = np.empty(n, dtype=dt)
|
|
2602
|
+
gap[...] = nd.NA
|
|
2603
|
+
values = [0, 1, 2, 3] + ([-1, -2] if np.dtype(t).kind == "i" else [])
|
|
2604
|
+
wrong = []
|
|
2605
|
+
for name in _c.binop_names:
|
|
2606
|
+
f = getattr(np, name)
|
|
2607
|
+
for v in values:
|
|
2608
|
+
other = np.full(n, v, dtype=t).astype(dt)
|
|
2609
|
+
for x, y, side in ((other, gap, "right"), (gap, other, "left")):
|
|
2610
|
+
try:
|
|
2611
|
+
with warnings.catch_warnings():
|
|
2612
|
+
warnings.simplefilter("error", RuntimeWarning)
|
|
2613
|
+
r = f(x, y)
|
|
2614
|
+
except RuntimeWarning as w:
|
|
2615
|
+
wrong.append((name, v, side, str(w)))
|
|
2616
|
+
continue
|
|
2617
|
+
except (TypeError, ValueError):
|
|
2618
|
+
continue
|
|
2619
|
+
if not nd.is_nullable(r.dtype):
|
|
2620
|
+
continue
|
|
2621
|
+
if r.dtype.wrapped.kind == "f":
|
|
2622
|
+
# computed in floats (`heaviside`, `float_power`, ...):
|
|
2623
|
+
# IEEE decides, as in `test_every_binop_keeps_a_gap`
|
|
2624
|
+
fv = np.full(n, v, dtype=np.float64)
|
|
2625
|
+
nan = np.full(n, np.nan)
|
|
2626
|
+
with np.errstate(all="ignore"):
|
|
2627
|
+
ref = f(fv, nan) if side == "right" else f(nan, fv)
|
|
2628
|
+
expect_na = np.isnan(ref) | (name in _DEPENDS_ON_GAP)
|
|
2629
|
+
if not (nd.isna(r) == expect_na).all():
|
|
2630
|
+
wrong.append((name, v, side, "float rule"))
|
|
2631
|
+
continue
|
|
2632
|
+
determined = name == "power" and (
|
|
2633
|
+
(side == "right" and v == 1) or (side == "left" and v == 0))
|
|
2634
|
+
if determined:
|
|
2635
|
+
if nd.isna(r).any() or not (nd.filled(r, 0) == 1).all():
|
|
2636
|
+
wrong.append((name, v, side, "expected 1"))
|
|
2637
|
+
elif not nd.isna(r).all():
|
|
2638
|
+
wrong.append((name, v, side, "expected NA"))
|
|
2639
|
+
assert not wrong
|
|
2640
|
+
|
|
2641
|
+
x = np.ones(3, dtype=dt)
|
|
2642
|
+
with pytest.warns(RuntimeWarning, match="divide by zero"):
|
|
2643
|
+
np.floor_divide(x, np.zeros(3, dtype=dt))
|
nulldtype-0.1.0/setup.py
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
|