8000 API,ENH: Change definition of complex sign and use it in copysign · numpy/numpy@37ee450 · GitHub
[go: up one dir, main page]

Skip to content

Commit 37ee450

Browse files
committed
API,ENH: Change definition of complex sign and use it in copysign
Following the API Array standard, the complex sign is now calculated as ``z / |z|`` (instead of the rather less logical case where the sign of the real part was taken, unless the real part was zero, in which case the sign of the imaginary part was returned). Like for real numbers, zero is returned if ``z==0``. With this, it has become possible to extend ``np.copysign(x1, x2)`` to complex numbers, since it can now generally return ``|x1| * sign(x2)`` with the sign as defined above (with no special treatment for zero).
1 parent 0032ede commit 37ee450

File tree

7 files changed

+97
-33
lines changed

7 files changed

+97
-33
lines changed

numpy/_core/code_generators/generate_umath.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1069,7 +1069,7 @@ def english_upper(s):
10691069
Ufunc(2, 1, None,
10701070
docstrings.get('numpy._core.umath.copysign'),
10711071
None,
1072-
TD(flts),
1072+
TD(inexact),
10731073
),
10741074
'nextafter':
10751075
Ufunc(2, 1, None,

numpy/_core/code_generators/ufunc_docstrings.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3540,10 +3540,11 @@ def add_newdoc(place, name, doc):
35403540
The `sign` function returns ``-1 if x < 0, 0 if x==0, 1 if x > 0``. nan
35413541
is returned for nan inputs.
35423542
3543-
For complex inputs, the `sign` function returns
3544-
``sign(x.real) + 0j if x.real != 0 else sign(x.imag) + 0j``.
3543+
For complex inputs, the `sign` function returns ``x / abs(x)``, the
3544+
generalization of the above (and ``0 if x==0``).
35453545
3546-
complex(nan, 0) is returned for complex nan inputs.
3546+
.. versionchanged:: 2.0.0
3547+
Definition of complex sign changed to follow the Array API standard.
35473548
35483549
Parameters
35493550
----------
@@ -3569,8 +3570,8 @@ def add_newdoc(place, name, doc):
35693570
array([-1., 1.])
35703571
>>> np.sign(0)
35713572
0
3572-
>>> np.sign(5-2j)
3573-
(1+0j)
3573+
>>> np.sign([3-4j, 8j])
3574+
array([0.6-0.8j, 0. +1.j ])
35743575
35753576
""")
35763577

@@ -3603,7 +3604,14 @@ def add_newdoc(place, name, doc):
36033604
"""
36043605
Change the sign of x1 to that of x2, element-wise.
36053606
3606-
If `x2` is a scalar, its sign will be copied to all elements of `x1`.
3607+
If `x2` is a scalar, its sign will be copied to all elements of `x1`,
3608+
i.e., the function returns ``abs(x1) * sign(x2)``, with the sign defined
3609+
generally as ``x2 / abs(x2)``. For the special case of ``x2 == 0``, for
3610+
real numbers the floating point sign of the zero is taken, while for
3611+
complex numbers the result is undefined (``nan+nanj``).
3612+
3613+
.. versionadded:: 2.0.0
3614+
Support complex numbers using the sign Array API definition of sign.
36073615
36083616
Parameters
36093617
----------

numpy/_core/function_base.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -430,26 +430,17 @@ def geomspace(start, stop, num=50, endpoint=True, dtype=None, axis=0):
430430
start = start.astype(dt, copy=True)
431431
stop = stop.astype(dt, copy=True)
432432

433-
out_sign = _nx.ones(_nx.broadcast(start, stop).shape, dt)
434-
# Avoid negligible real or imaginary parts in output by rotating to
435-
# positive real, calculating, then undoing rotation
436-
if _nx.issubdtype(dt, _nx.complexfloating):
437-
all_imag = (start.real == 0.) & (stop.real == 0.)
438-
if _nx.any(all_imag):
439-
start[all_imag] = start[all_imag].imag
440-
stop[all_imag] = stop[all_imag].imag
441-
out_sign[all_imag] = 1j
442-
443-
both_negative = (_nx.sign(start) == -1) & (_nx.sign(stop) == -1)
444-
if _nx.any(both_negative):
445-
_nx.negative(start, out=start, where=both_negative)
446-
_nx.negative(stop, out=stop, where=both_negative)
447-
_nx.negative(out_sign, out=out_sign, where=both_negative)
433+
# Allow negative real values and ensure a consistent result for complex
434+
# (including avoiding negligible real or imaginary parts in output) by
435+
# rotating start to positive real, calculating, then undoing rotation.
436+
out_sign = _nx.sign(start)
437+
start /= out_sign
438+
stop = stop / out_sign
448439

449440
log_start = _nx.log10(start)
450441
log_stop = _nx.log10(stop)
451442
result = logspace(log_start, log_stop, num=num,
452-
endpoint=endpoint, base=10.0, dtype=dtype)
443+
endpoint=endpoint, base=10.0, dtype=dt)
453444

454445
# Make sure the endpoints match the start and stop arguments. This is
455446
# necessary because np.exp(np.log(x)) is not necessarily equal to x.
@@ -458,7 +449,7 @@ def geomspace(start, stop, num=50, endpoint=True, dtype=None, axis=0):
458449
if num > 1 and endpoint:
459450
result[-1] = stop
460451

461-
result = out_sign * result
452+
result *= out_sign
462453

463454
if axis != 0:
464455
result = _nx.moveaxis(result, 0, axis)

numpy/_core/src/umath/loops.c.src

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2232,14 +2232,53 @@ NPY_NO_EXPORT void
22322232
NPY_NO_EXPORT void
22332233
@TYPE@_sign(char **args, npy_intp const *dimensions, npy_intp const *steps, void *NPY_UNUSED(func))
22342234
{
2235-
/* fixme: sign of nan is currently 0 */
22362235
UNARY_LOOP {
22372236
const @ftype@ in1r = ((@ftype@ *)ip1)[0];
22382237
const @ftype@ in1i = ((@ftype@ *)ip1)[1];
2239-
((@ftype@ *)op1)[0] = CGT(in1r, in1i, 0.0, 0.0) ? 1 :
2240-
(CLT(in1r, in1i, 0.0, 0.0) ? -1 :
2241-
(CEQ(in1r, in1i, 0.0, 0.0) ? 0 : NPY_NAN@C@));
2242-
((@ftype@ *)op1)[1] = 0;
2238+
const @ftype@ abs1 = npy_hypot@c@(in1r, in1i);
2239+
if (NPY_UNLIKELY(npy_isnan(abs1))) {
2240+
((@ftype@ *)op1)[0] = NPY_NAN@C@;
2241+
((@ftype@ *)op1)[1] = NPY_NAN@C@;
2242+
}
2243+
else if (NPY_UNLIKELY(npy_isinf(abs1))) {
2244+
if (npy_isinf(in1r)) {
2245+
if (npy_isinf(in1i)) {
2246+
((@ftype@ *)op1)[0] = NPY_NAN@C@;
2247+
((@ftype@ *)op1)[1] = NPY_NAN@C@;
2248+
}
2249+
else {
2250+
((@ftype@ *)op1)[0] = in1r > 0 ? 1.: -1.;
2251+
((@ftype@ *)op1)[1] = 0.;
2252+
}
2253+
}
2254+
else {
2255+
((@ftype@ *)op1)[0] = 0.;
2256+
((@ftype@ *)op1)[1] = in1i > 0 ? 1.: -1.;
2257+
}
2258+
}
2259+
else if (NPY_UNLIKELY(abs1 == 0)) {
2260+
((@ftype@ *)op1)[0] = 0.;
2261+
((@ftype@ *)op1)[1] = 0.;
2262+
}
2263+
else{
2264+
((@ftype@ *)op1)[0] = in1r / abs1;
2265+
((@ftype@ *)op1)[1] = in1i / abs1;
2266+
}
2267+
}
2268+
}
2269+
2270+
NPY_NO_EXPORT void
2271+
@TYPE@_copysign(char **args, npy_intp const *dimensions, npy_intp const *steps, void *NPY_UNUSED(func))
2272+
{
2273+
BINARY_LOOP {
2274+
const @ftype@ in1r = ((@ftype@ *)ip1)[0];
2275+
const @ftype@ in1i = ((@ftype@ *)ip1)[1];
2276+
const @ftype@ in2r = ((@ftype@ *)ip2)[0];
2277+
const @ftype@ in2i = ((@ftype@ *)ip2)[1];
2278+
const @ftype@ abs1 = npy_hypot@c@(in1r, in1i);
2279+
const @ftype@ abs2 = npy_hypot@c@(in2r, in2i);
2280+
((@ftype@ *)op1)[0] = abs1 * (in2r / abs2);
2281+
((@ftype@ *)op1)[1] = abs1 * (in2i / abs2);
22432282
}
22442283
}
22452284

numpy/_core/src/umath/loops.h.src

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,9 @@ C@TYPE@__arg(char **args, npy_intp const *dimensions, npy_intp const *steps, voi
666666
NPY_NO_EXPORT void
667667
C@TYPE@_sign(char **args, npy_intp const *dimensions, npy_intp const *steps, void *NPY_UNUSED(func));
668668

669+
NPY_NO_EXPORT void
670+
C@TYPE@_copysign(char **args, npy_intp const *dimensions, npy_intp const *steps, void *NPY_UNUSED(func));
671+
669672
/**begin repeat1
670673
* #kind = maximum, minimum#
671674
* #OP = CGE, CLE#

numpy/_core/tests/test_regression.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,9 +1189,10 @@ def test_unaligned_unicode_access(self):
11891189
def test_sign_for_complex_nan(self):
11901190
# Ticket 794.
11911191
with np.errstate(invalid='ignore'):
1192-
C = np.array([-np.inf, -2+1j, 0, 2-1j, np.inf, np.nan])
1192+
C = np.array([-np.inf, -3+4j, 0, 4-3j, np.inf, np.nan])
11931193
have = np.sign(C)
1194-
want = np.array([-1+0j, -1+0j, 0+0j, 1+0j, 1+0j, np.nan])
1194+
want = np.array([-1+0j, -0.6+0.8j, 0+0j, 0.8-0.6j, 1+0j,
1195+
complex(np.nan, np.nan)])
11951196
assert_equal(have, want)
11961197

11971198
def test_for_equal_names(self):
@@ -1481,7 +1482,7 @@ def test_buffer_hashlib(self):
14811482

14821483
x = np.array([1, 2, 3], dtype=np.dtype('<i4'))
14831484
assert_equal(
1484-
sha256(x).hexdigest(),
1485+
sha256(x).hexdigest(),
14851486
'4636993d3e1da4e9d6b8f87b79e8f7c6d018580d52661950eabc3845c5897a4d'
14861487
)
14871488

@@ -1941,7 +1942,7 @@ def test_pickle_py2_scalar_latin1_hack(self):
19411942
'invalid'),
19421943

19431944
# different 8-bit code point in KOI8-R vs latin1
1944-
(np.bytes_(b'\x9c'),
1945+
(np.bytes_(b'\x9c'),
19451946
b"cnumpy.core.multiarray\nscalar\np0\n(cnumpy\ndtype\np1\n(S'S1'\np2\nI0\nI1\ntp3\nRp4\n(I3\nS'|'\np5\nNNNI1\nI1\nI0\ntp6\nbS'\\x9c'\np7\ntp8\nRp9\n.", # noqa
19461947
'different'),
19471948
]

numpy/_core/tests/test_umath.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2825,6 +2825,28 @@ def test_sign(self):
28252825
assert_equal(res, tgt)
28262826
assert_equal(out, tgt)
28272827

2828+
def test_sign_complex(self):
2829+
a = np.array([
2830+
np.inf, -np.inf, complex(0, np.inf), complex(0, -np.inf),
2831+
complex(np.inf, np.inf), complex(np.inf, -np.inf), # nan
2832+
np.nan, complex(0, np.nan), complex(np.nan, np.nan), # nan
2833+
0.0, # 0.
2834+
3.0, -3.0, -2j, 3.0+4.0j, -8.0+6.0j
2835+
])
2836+
out = np.zeros(a.shape, a.dtype)
2837+
tgt = np.array([
2838+
1., -1., 1j, -1j,
2839+
] + [complex(np.nan, np.nan)] * 5 + [
2840+
0.0,
2841+
1.0, -1.0, -1j, 0.6+0.8j, -0.8+0.6j])
2842+
2843+
with np.errstate(invalid='ignore'):
2844+
res = ncu.sign(a)
2845+
assert_equal(res, tgt)
2846+
res = ncu.sign(a, out)
2847+
assert_(res is out)
2848+
assert_equal(res, tgt)
2849+
28282850
def test_sign_dtype_object(self):
28292851
# In reference to github issue #6229
28302852

0 commit comments

Comments
 (0)
0