8000 BUG: Prevent power(uint64, int64) casting to float · numpy/numpy@fee4481 · GitHub
[go: up one dir, main page]

Skip to content

Commit fee4481

Browse files
committed
BUG: Prevent power(uint64, int64) casting to float
Partials resolves #8809 Previously, the signatures were: * power(int, int) -> int * power(uint, uint) -> uint. Since we forbid the second argument from being negative, we can also define * power(int, uint) -> int: same as power(int, int), without an error check * power(uint, int) -> uint: same as power(uint, uint), with an error check If we do not define these, then the signature used is: * t = common(int, uint); power(t, t) -> t: This is bad, because common(int64, uint64) is float
1 parent 1e8143c commit fee4481

File tree

6 files changed

+127
-59
lines changed

6 files changed

+127
-59
lines changed

doc/release/1.13.0-notes.rst

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,27 @@ offset into the file. This is a behaviour change only for offsets
356356
greater than ``mmap.ALLOCATIONGRANULARITY``.
357357

358358
``np.real`` and ``np.imag`` return scalars for scalar inputs
359-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
359+
------------------------------------------------------------
360360
Previously, ``np.real`` and ``np.imag`` used to return array objects when
361361
provided a scalar input, which was inconsistent with other functions like
362362
``np.angle`` and ``np.conj``.
363+
364+
``np.power`` does not upcast integer types of the same size but different sign
365+
------------------------------------------------------------------------------
366+
Previously, ``np.power(uint, int)``, for some sized integer type would return a
367+
larger type than either ``uint`` or ``int``. This caused undesirable decaying
368+
to a ``float64`` when working with ``uint64`` and ``int64``.
369+
370+
A consequence of this is that some overflows are more likely to occur
371+
.. code::
372+
373+
>>> u8 = np.uint8
374+
>>> s8 = np.int8
375+
>>> np.power(s8(2), s8(7))
376+
-128 # same as before
377+
>>> np.power(u8(2), u8(7))
378+
128 # same as before
379+
>>> np.power(s8(2), u8(7))
380+
-128 # now int8, previously int16 and did not overflow
381+
>>> np.power(u8(2), s8(7))
382+
128 # now uint8, previously int16

numpy/core/code_generators/generate_umath.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,10 @@ def english_upper(s):
366366
Ufunc(2, 1, None,
367367
docstrings.get('numpy.core.umath.power'),
368368
None,
369-
TD(ints),
369+
[TypeDescription(i, FullTypeDescr, i+i2, i)
370+
for i in ints
371+
for i2 in [i.lower(), i.upper()]
372+
],
370373
TD(inexact, f='pow', astype={'e':'f'}),
371374
TD(O, f='npy_ObjectPower'),
372375
),

numpy/core/src/umath/loops.c.src

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -952,45 +952,6 @@ NPY_NO_EXPORT void
952952
}
953953
}
954954

955-
NPY_NO_EXPORT void
956-
@TYPE@_power(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func))
957-
{
958-
BINARY_LOOP {
959-
@type@ in1 = *(@type@ *)ip1;
960-
@type@ in2 = *(@type@ *)ip2;
961-
@type@ out;
962-
963-
#if @SIGNED@
964-
if (in2 < 0) {
965-
NPY_ALLOW_C_API_DEF
966-
NPY_ALLOW_C_API;
967-
PyErr_SetString(PyExc_ValueError,
968-
"Integers to negative integer powers are not allowed.");
969-
NPY_DISABLE_C_API;
970-
return;
971-
}
972-
#endif
973-
if (in2 == 0) {
974-
*((@type@ *)op1) = 1;
975-
continue;
976-
}
977-
if (in1 == 1) {
978-
*((@type@ *)op1) = 1;
979-
continue;
980-
}
981-
982-
out = in2 & 1 ? in1 : 1;
983-
in2 >>= 1;
984-
while (in2 > 0) {
985-
in1 *= in1;
986-
if (in2 & 1) {
987-
out *= in1;
988-
}
989-
in2 >>= 1;
990-
}
991-
*((@type@ *) op1) = out;
992-
}
993-
}
994955

995956
NPY_NO_EXPORT void
996957
@TYPE@_fmod(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func))
@@ -1133,6 +1094,61 @@ NPY_NO_EXPORT void
11331094

11341095
/**end repeat**/
11351096

1097+
1098+
/**begin repeat
1099+
* #TYPE = ( BYTE, SHORT, INT, LONG, LONGLONG,
1100+
* UBYTE, USHORT, UINT, ULONG, ULONGLONG)*2#
1101+
* #C = (b,h,i,l,q,
1102+
* B,H,I,L,Q)*2#
1103+
* #C2 = (b,h,i,l,q)*2,
1104+
* (B,H,I,L,Q)*2#
1105+
* #type = (npy_byte , npy_short , npy_int , npy_long , npy_longlong,
1106+
* npy_ubyte, npy_ushort, npy_uint, npy_ulong, npy_ulonglong)*2#
1107+
* #type2 = (npy_byte , npy_short , npy_int , npy_long , npy_longlong )*2,
1108+
* (npy_ubyte, npy_ushort, npy_uint, npy_ulong, npy_ulonglong)*2#
1109+
* #SIGNED2 = 1*10,10*10#
1110+
*/
1111+
NPY_NO_EXPORT void
1112+
@TYPE@_@C@@C2@_@C@_power(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func))
1113+
{
1114+
BINARY_LOOP {
1115+
@type@ in1 = *(@type@ *)ip1;
1116+
@type2@ in2 = *(@type2@ *)ip2;
1117+
@type@ out;
1118+
1119+
#if @SIGNED2@
1120+
if (in2 < 0) {
1121+
NPY_ALLOW_C_API_DEF
1122+
NPY_ALLOW_C_API;
1123+
PyErr_SetString(PyExc_ValueError,
1124+
"Integers to negative integer powers are not allowed.");
1125+
NPY_DISABLE_C_API;
1126+
return;
1127+
}
1128+
#endif
1129+
if (in2 == 0) {
1130+
*((@type@ *)op1) = 1;
1131+
continue;
1132+
}
1133+
if (in1 == 1) {
1134+
*((@type@ *)op1) = 1;
1135+
continue;
1136+
}
1137+
1138+
out = in2 & 1 ? in1 : 1;
1139+
in2 >>= 1;
1140+
while (in2 > 0) {
1141+
in1 *= in1;
1142+
if (in2 & 1) {
1143+
out *= in1;
1144+
}
1145+
in2 >>= 1;
1146+
}
1147+
*((@type@ *) op1) = out;
1148+
}
1149+
}
1150+
/**end repeat**/
1151+
11361152
/*
11371153
*****************************************************************************
11381154
** DATETIME LOOPS **

numpy/core/src/umath/loops.h.src

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,23 @@ NPY_NO_EXPORT void
152152
U@TYPE@_remainder(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func));
153153
/**end repeat**/
154154

155+
/**begin repeat
156+
* #TYPE = ( BYTE, SHORT, INT, LONG, LONGLONG,
157+
* UBYTE, USHORT, UINT, ULONG, ULONGLONG)*2#
158+
* #C = (b,h,i,l,q,
159+
* B,H,I,L,Q)*2#
160+
* #C2 = (b,h,i,l,q)*2,
161+
* (B,H,I,L,Q)*2#
162+
* #type = (npy_byte , npy_short , npy_int , npy_long , npy_longlong,
163+
* npy_ubyte, npy_ushort, npy_uint, npy_ulong, npy_ulonglong)*2#
164+
* #type2 = (npy_byte , npy_short , npy_int , npy_long , npy_longlong )*2,
165+
* (npy_ubyte, npy_ushort, npy_uint, npy_ulong, npy_ulonglong)*2#
166+
* #SIGNED2 = 0*10,1*10#
167+
*/
168+
NPY_NO_EXPORT void
169+
@TYPE@_@C@@C2@_@C@_power(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func));
170+
/**end repeat**/
171+
155172
/*
156173
*****************************************************************************
157174
** FLOAT LOOPS **

numpy/core/tests/test_scalarmath.py

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -134,32 +134,17 @@ def test_integers_to_negative_integer_power(self):
134134
# 1 ** -1 possible special case
135135
base = [np.array(1, dt)[()] for dt in 'bhilqBHILQ']
136136
for i1, i2 in itertools.product(base, exp):
137-
if i1.dtype.name != 'uint64':
138-
assert_raises(ValueError, operator.pow, i1, i2)
139-
else:
140-
res = operator.pow(i1, i2)
141-
assert_(res.dtype.type is np.float64)
142-
assert_almost_equal(res, 1.)
137+
assert_raises(ValueError, operator.pow, i1, i2)
143138

144139
# -1 ** -1 possible special case
145140
base = [np.array(-1, dt)[()] for dt in 'bhilq']
146141
for i1, i2 in itertools.product(base, exp):
147-
if i1.dtype.name != 'uint64':
148-
assert_raises(ValueError, operator.pow, i1, i2)
149-
else:
150-
res = operator.pow(i1, i2)
151-
assert_(res.dtype.type is np.float64)
152-
assert_almost_equal(res, -1.)
142+
assert_raises(ValueError, operator.pow, i1, i2)
153143

154144
# 2 ** -1 perhaps generic
155145
base = [np.array(2, dt)[()] for dt in 'bhilqBHILQ']
156146
for i1, i2 in itertools.product(base, exp):
157-
if i1.dtype.name != 'uint64':
158-
assert_raises(ValueError, operator.pow, i1, i2)
159-
else:
160-
res = operator.pow(i1, i2)
161-
assert_(res.dtype.type is np.float64)
162-
assert_almost_equal(res, .5)
147+
assert_raises(ValueError, operator.pow, i1, i2)
163148

164149
def test_mixed_types(self):
165150
typelist = [np.int8, np.int16, np.float16,

numpy/core/tests/test_umath.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,33 @@ def test_integer_to_negative_power(self):
502502
assert_raises(ValueError, np.power, one, b)
503503
assert_raises(ValueError, np.power, one, minusone)
504504

505+
def test_power_mixedsign(self):
506+
"""
507+
When given a signed and unsigned integer of the same size, the
508+
result match the type of the first argument
509+
"""
510+
inttypes = [
511+
(np.int8, np.uint8),
512+
(np.int16, np.uint16),
513+
(np.int32, np.uint32),
514+
(np.int64, np.uint64)
515+
]
516+
for stype, utype in inttypes:
517+
av = stype(6)
518+
bv = utype(2)
519+
520+
for conv in [lambda x: x, np.array, lambda x: np.array([x])]:
521+
a = conv(av)
522+
b = conv(bv)
523+
524+
apb = np.power(a, b)
525+
assert_equal(apb.dtype, a.dtype)
526+
assert_equal(apb, 36)
527+
528+
bpa = np.power(b, a)
529+
assert_equal(bpa.dtype, b.dtype)
530+
assert_equal(bpa, 64)
531+
505532

506533
class TestFloat_power(TestCase):
507534
def test_type_conversion(self):

0 commit comments

Comments
 (0)
0