Skip to content

Commit 5e40326

Browse files
authored
BUG: Relax finfo to be easier accessible for all user dtypes (numpy#31563)
This allows `finfo` to be used for downstream user dtypes that do not subclass `inexact` or are "complex". Where complex is defined by whether `arr.imag/arr.real` return the same "real" dtype correspondance.
1 parent 8680b48 commit 5e40326

4 files changed

Lines changed: 128 additions & 17 deletions

File tree

doc/source/reference/c-api/array.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3732,6 +3732,11 @@ member of ``PyArrayDTypeMeta_Spec`` struct.
37323732
Returns 1 on success, 0 if the constant is not available,
37333733
or -1 with an error set.
37343734
3735+
Implementing all ``finfo`` constants allows the DType to be used together
3736+
with `numpy.finfo`. Complex dtypes can support ``finfo`` by implementing the
3737+
``imag`` and ``real`` slots (see :c:func:`PyUFunc_AddLoopFromSpec`) when
3738+
the corresponding real DType implements it.
3739+
37353740
**Constant IDs**:
37363741
37373742
The following constant IDs are defined for retrieving dtype-specific values:

numpy/_core/getlimits.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from numpy._utils import set_module
1111

1212
from . import numeric, numerictypes as ntypes
13-
from ._multiarray_umath import _populate_finfo_constants
13+
from ._multiarray_umath import _finfo_get_realdtype, _populate_finfo_constants
1414

1515

1616
def _fr0(a):
@@ -27,12 +27,6 @@ def _fr1(a):
2727
return a
2828

2929

30-
_convert_to_float = {
31-
ntypes.csingle: ntypes.single,
32-
ntypes.complex128: ntypes.float64,
33-
ntypes.clongdouble: ntypes.longdouble
34-
}
35-
3630
# Parameters for creating MachAr / MachAr-like objects
3731
_title_fmt = 'numpy {} precision floating point number'
3832
_MACHAR_PARAMS = {
@@ -193,17 +187,19 @@ def __new__(cls, dtype):
193187
if obj is not None:
194188
return obj
195189
dtypes = [dtype]
196-
newdtype = ntypes.obj2sctype(dtype)
190+
# Call result_type to normalize to e.g. native byte-order:
191+
newdtype = numeric.result_type(dtype)
197192
if newdtype is not dtype:
198193
dtypes.append(newdtype)
199194
dtype = newdtype
200-
if not issubclass(dtype, numeric.inexact):
201-
raise ValueError(f"data type {dtype!r} not inexact")
195+
202196
obj = cls._finfo_cache.get(dtype)
203197
if obj is not None:
204198
return obj
205-
if not issubclass(dtype, numeric.floating):
206-
newdtype = _convert_to_float[dtype]
199+
200+
sctype = newdtype.type
201+
if sctype is not None and not issubclass(sctype, numeric.floating):
202+
newdtype = _finfo_get_realdtype(dtype)
207203
if newdtype is not dtype:
208204
# dtype changed, for example from complex128 to float64
209205
dtypes.append(newdtype)

numpy/_core/src/multiarray/multiarraymodule.c

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4350,6 +4350,97 @@ normalize_axis_index(PyObject *NPY_UNUSED(self),
43504350
}
43514351

43524352

4353+
static int
4354+
resolve_part_view_descr(
4355+
PyBoundArrayMethodObject *meth, PyArray_Descr *descr,
4356+
PyArray_Descr **part_descr, npy_intp *view_offset)
4357+
{
4358+
PyArray_Descr *descrs[2] = {descr, NULL};
4359+
PyArray_Descr *loop_descrs[2] = {NULL, NULL};
4360+
int res = meth->method->resolve_descriptors(
4361+
meth->method, meth->dtypes, descrs, loop_descrs, view_offset);
4362+
if (res < 0) {
4363+
return -1;
4364+
}
4365+
4366+
Py_DECREF(loop_descrs[0]);
4367+
*part_descr = loop_descrs[1];
4368+
return 0;
4369+
}
4370+
4371+
4372+
/*
4373+
* Resolve the descriptor for a dtype's `.real` or `.imag` method and
4374+
* indicate whether the result is a view (1), not a view (0), or errored (-1).
4375+
*/
4376+
static int
4377+
resolve_view_part_descr(
4378+
PyBoundArrayMethodObject *meth, PyArray_Descr *descr,
4379+
PyArray_Descr **part_descr)
4380+
{
4381+
if (meth == NULL) {
4382+
return 0;
4383+
}
4384+
npy_intp view_offset = NPY_MIN_INTP;
4385+
if (resolve_part_view_descr(meth, descr, part_descr, &view_offset) < 0) {
4386+
return -1;
4387+
}
4388+
return view_offset != NPY_MIN_INTP;
4389+
}
4390+
4391+
4392+
/*
4393+
* Resolve the real counterpart dtype for dtypes that expose compatible
4394+
* `.real`/`.imag` views. If not available, returns the input dtype unchanged
4395+
* (i.e. assume already a real dtype).
4396+
*/
4397+
static PyObject *
4398+
_finfo_get_realdtype(PyObject *NPY_UNUSED(self), PyObject *descr_obj)
4399+
{
4400+
if (!PyArray_DescrCheck(descr_obj)) {
4401+
PyErr_SetString(PyExc_TypeError, "Argument must be a dtype");
4402+
return NULL;
4403+
}
4404+
PyArray_Descr *descr = (PyArray_Descr *)descr_obj;
4405+
PyArray_Descr *real_descr = NULL;
4406+
PyArray_Descr *imag_descr = NULL;
4407+
PyArray_Descr *ret = NULL;
4408+
4409+
int real_is_view = resolve_view_part_descr(
4410+
NPY_DT_SLOTS(NPY_DTYPE(descr))->real_meth, descr, &real_descr);
4411+
if (real_is_view < 0) {
4412+
goto finish;
4413+
}
4414+
if (!real_is_view) {
4415+
ret = descr;
4416+
goto finish;
4417+
}
4418+
4419+
int imag_is_view = resolve_view_part_descr(
4420+
NPY_DT_SLOTS(NPY_DTYPE(descr))->imag_meth, descr, &imag_descr);
4421+
if (imag_is_view < 0) {
4422+
goto finish;
4423+
}
4424+
if (!imag_is_view) {
4425+
ret = descr;
4426+
goto finish;
4427+
}
4428+
4429+
if (PyArray_EquivTypes(real_descr, imag_descr)) {
4430+
ret = real_descr;
4431+
}
4432+
else {
4433+
ret = descr;
4434+
}
4435+
4436+
finish:
4437+
Py_XINCREF(ret);
4438+
Py_XDECREF(real_descr);
4439+
Py_XDECREF(imag_descr);
4440+
return (PyObject *)ret;
4441+
}
4442+
4443+
43534444
static PyObject *
43544445
_populate_finfo_constants(PyObject *NPY_UNUSED(self), PyObject *args)
43554446
{
@@ -4416,8 +4507,9 @@ _populate_finfo_constants(PyObject *NPY_UNUSED(self), PyObject *args)
44164507
goto fail;
44174508
}
44184509
if (res == 0) {
4419-
buffer_data += elsize; // Move to next element
4420-
continue;
4510+
PyErr_Format(PyExc_ValueError,
4511+
"data type %R not compatible with finfo", descr);
4512+
goto fail;
44214513
}
44224514
// Return as 0-d array item to preserve numpy scalar type
44234515
value_obj = PyArray_ToScalar(buffer_data, buffer_array);
@@ -4430,7 +4522,9 @@ _populate_finfo_constants(PyObject *NPY_UNUSED(self), PyObject *args)
44304522
goto fail;
44314523
}
44324524
if (res == 0) {
4433-
continue;
4525+
PyErr_Format(PyExc_ValueError,
4526+
"data type %R not compatible with finfo", descr);
4527+
goto fail;
44344528
}
44354529
value_obj = PyLong_FromSsize_t(int_value);
44364530
}
@@ -4702,6 +4796,8 @@ static struct PyMethodDef array_module_methods[] = {
47024796
METH_FASTCALL | METH_KEYWORDS, NULL},
47034797
{"_populate_finfo_constants", (PyCFunction)_populate_finfo_constants,
47044798
METH_VARARGS, NULL},
4799+
{"_finfo_get_realdtype", (PyCFunction)_finfo_get_realdtype,
4800+
METH_O, NULL},
47054801
/* from umath */
47064802
{"frompyfunc",
47074803
(PyCFunction) ufunc_frompyfunc,

numpy/_core/tests/test_getlimits.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import numpy as np
1010
from numpy import double, half, longdouble, single
1111
from numpy._core import finfo, iinfo
12+
from numpy._core._rational_tests import rational
1213
from numpy.testing import assert_, assert_equal, assert_raises
1314

1415
##################################################
@@ -48,7 +49,7 @@ def assert_finfo_equal(f1, f2):
4849
for attr in ('bits', 'eps', 'epsneg', 'iexp', 'machep',
4950
'max', 'maxexp', 'min', 'minexp', 'negep', 'nexp',
5051
'nmant', 'precision', 'resolution', 'tiny',
51-
'smallest_normal', 'smallest_subnormal'):
52+
'smallest_normal', 'smallest_subnormal', 'dtype'):
5253
assert_equal(getattr(f1, attr), getattr(f2, attr),
5354
f'finfo instances {f1} and {f2} differ on {attr}')
5455

@@ -66,7 +67,20 @@ def test_basic(self):
6667
for dt1, dt2 in dts:
6768
assert_finfo_equal(finfo(dt1), finfo(dt2))
6869

69-
assert_raises(ValueError, finfo, 'i4')
70+
@pytest.mark.parametrize('dt1, dt2',
71+
[('>f2', '<f2'), ('>f4', '<f4'), ('>f8', '<f8'), ('>c8', '<c8'),
72+
('>c16', '<c16')])
73+
def test_byteorder(self, dt1, dt2):
74+
# finfo should normalize to native byte-order.
75+
assert_finfo_equal(finfo(dt1), finfo(dt2))
76+
77+
@pytest.mark.parametrize('dt', [
78+
np.int8, "V3", "S3", "f,f", rational, "O", "T"])
79+
def test_rejects_others(self, dt):
80+
dtype = np.dtype(dt)
81+
with pytest.raises(ValueError,
82+
match=r"data type .* not compatible with finfo"):
83+
finfo(dtype)
7084

7185
def test_regression_gh23108(self):
7286
# np.float32(1.0) and np.float64(1.0) have the same hash and are

0 commit comments

Comments
 (0)