Skip to content

Commit 50f3fd3

Browse files
authored
Merge pull request #190 from SwayamInSync/nextafter
2 parents a160555 + e325053 commit 50f3fd3

File tree

4 files changed

+326
-1
lines changed

4 files changed

+326
-1
lines changed

quaddtype/numpy_quaddtype/src/ops.hpp

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,131 @@ quad_hypot(const Sleef_quad *x1, const Sleef_quad *x2)
883883
return Sleef_hypotq1_u05(*x1, *x2);
884884
}
885885

886+
// todo: we definitely need to refactor this file, getting too clumsy everything here
887+
888+
static inline void quad_get_words64(int64_t *hx, uint64_t *lx, Sleef_quad x)
889+
{
890+
union {
891+
Sleef_quad q;
892+
struct {
893+
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
894+
uint64_t hi;
895+
uint64_t lo;
896+
#else
897+
uint64_t lo;
898+
uint64_t hi;
899+
#endif
900+
} i;
901+
} u;
902+
u.q = x;
903+
*hx = (int64_t)u.i.hi;
904+
*lx = u.i.lo;
905+
}
906+
907+
static inline Sleef_quad quad_set_words64(int64_t hx, uint64_t lx)
908+
{
909+
union {
910+
Sleef_quad q;
911+
struct {
912+
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
913+
uint64_t hi;
914+
uint64_t lo;
915+
#else
916+
uint64_t lo;
917+
uint64_t hi;
918+
#endif
919+
} i;
920+
} u;
921+
u.i.hi = (uint64_t)hx;
922+
u.i.lo = lx;
923+
return u.q;
924+
}
925+
926+
927+
static inline Sleef_quad
928+
quad_nextafter(const Sleef_quad *x, const Sleef_quad *y)
929+
{
930+
int64_t hx, hy, ix, iy;
931+
uint64_t lx, ly;
932+
933+
quad_get_words64(&hx, &lx, *x);
934+
quad_get_words64(&hy, &ly, *y);
935+
936+
// extracting absolute value
937+
ix = hx & 0x7fffffffffffffffLL;
938+
iy = hy & 0x7fffffffffffffffLL;
939+
940+
// NaN if either is NaN
941+
if (Sleef_iunordq1(*x, *y)) {
942+
return Sleef_addq1_u05(*x, *y); // still NaN
943+
}
944+
945+
// x == y then return y
946+
if (Sleef_icmpeqq1(*x, *y)) {
947+
return *y;
948+
}
949+
950+
// both input 0 then extract sign from y and return correspondingly signed smallest subnormal
951+
if ((ix | lx) == 0) {
952+
Sleef_quad result = quad_set_words64(hy & 0x8000000000000000LL, 1); // quad_set_words64(sign_y, 1)
953+
return result;
954+
}
955+
956+
if (hx >= 0)
957+
{
958+
if (hx > hy || ((hx == hy) && (lx > ly)))
959+
{
960+
// Moving toward smaller y (x > y)
961+
// low word is 0 then decrement high word first (borrowing)
962+
if (lx == 0)
963+
hx--;
964+
lx--;
965+
}
966+
else
967+
{
968+
lx++;
969+
if (lx == 0)
970+
// carry to high words
971+
hx++;
972+
}
973+
}
974+
else
975+
{
976+
// Moving toward larger y
977+
// similar to above case just direction will be swapped
978+
if (hy >= 0 || hx > hy || ((hx == hy) && (lx > ly)))
979+
{
980+
if (lx == 0)
981+
hx--;
982+
lx--;
983+
}
984+
else
985+
{
986+
lx++;
987+
if (lx == 0)
988+
hx++;
989+
}
990+
}
991+
992+
// check if reached infinity
993+
// this can be NaN XOR inf but NaN are already checked at start
994+
hy = hx & 0x7fff000000000000LL;
995+
if (hy == 0x7fff000000000000LL) {
996+
Sleef_quad result = quad_set_words64(hx, lx);
997+
return result;
998+
}
999+
// check whether entered into subnormal range
1000+
// 0 exponent i.e. either (0 or subnormal)
1001+
if (hy == 0) {
1002+
Sleef_quad result = quad_set_words64(hx, lx);
1003+
return result;
1004+
}
1005+
1006+
// well I did not need to have those above checks
1007+
// but they can be important when setting FPE flag manually
1008+
return quad_set_words64(hx, lx);
1009+
}
1010+
8861011
// Binary long double operations
8871012
typedef long double (*binary_op_longdouble_def)(const long double *, const long double *);
8881013
// Binary long double operations with 2 outputs (for divmod, modf, frexp)
@@ -1161,6 +1286,12 @@ ld_hypot(const long double *x1, const long double *x2)
11611286
return hypotl(*x1, *x2);
11621287
}
11631288

1289+
static inline long double
1290+
ld_nextafter(const long double *x1, const long double *x2)
1291+
{
1292+
return nextafterl(*x1, *x2);
1293+
}
1294+
11641295
// comparison quad functions
11651296
typedef npy_bool (*cmp_quad_def)(const Sleef_quad *, const Sleef_quad *);
11661297

quaddtype/numpy_quaddtype/src/umath/binary_ops.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,9 @@ init_quad_binary_ops(PyObject *numpy)
448448
if (create_quad_binary_ufunc<quad_copysign, ld_copysign>(numpy, "copysign") < 0) {
449449
return -1;
450450
}
451+
if (create_quad_binary_ufunc<quad_nextafter, ld_nextafter>(numpy, "nextafter") < 0) {
452+
return -1;
453+
}
451454
if (create_quad_binary_ufunc<quad_logaddexp, ld_logaddexp>(numpy, "logaddexp") < 0) {
452455
return -1;
453456
}

quaddtype/release_tracker.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
| isnan |||
7878
| signbit |||
7979
| copysign |||
80-
| nextafter | | |
80+
| nextafter | | |
8181
| spacing | | |
8282
| modf | | |
8383
| ldexp | | |

quaddtype/tests/test_quaddtype.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2065,3 +2065,194 @@ def test_radians(op, degrees, expected_radians):
20652065
else:
20662066
np.testing.assert_allclose(float(result), expected_radians, rtol=1e-13)
20672067

2068+
2069+
class TestNextAfter:
2070+
"""Test cases for np.nextafter function with QuadPrecision dtype"""
2071+
2072+
@pytest.mark.parametrize("x1,x2", [
2073+
# NaN tests
2074+
(np.nan, 1.0),
2075+
(1.0, np.nan),
2076+
(np.nan, np.nan),
2077+
])
2078+
def test_nan(self, x1, x2):
2079+
"""Test nextafter with NaN inputs returns NaN"""
2080+
q_x1 = QuadPrecision(x1)
2081+
q_x2 = QuadPrecision(x2)
2082+
2083+
result = np.nextafter(q_x1, q_x2)
2084+
2085+
assert isinstance(result, QuadPrecision)
2086+
assert np.isnan(float(result))
2087+
2088+
def test_precision(self):
2089+
"""Test that nextafter provides the exact next representable value"""
2090+
# Start with 1.0 and move towards 2.0
2091+
x1 = QuadPrecision(1.0)
2092+
x2 = QuadPrecision(2.0)
2093+
2094+
result = np.nextafter(x1, x2)
2095+
2096+
# Get machine epsilon from finfo
2097+
finfo = np.finfo(QuadPrecDType())
2098+
expected = x1 + finfo.eps
2099+
2100+
# result should be exactly 1.0 + eps
2101+
assert result == expected
2102+
2103+
# Moving the other direction should give us back 1.0
2104+
result_back = np.nextafter(result, x1)
2105+
assert result_back == x1
2106+
2107+
def test_smallest_subnormal(self):
2108+
"""Test that nextafter(0.0, 1.0) returns the smallest positive subnormal (TRUE_MIN)"""
2109+
zero = QuadPrecision(0.0)
2110+
one = QuadPrecision(1.0)
2111+
2112+
result = np.nextafter(zero, one) # smallest_subnormal
2113+
finfo = np.finfo(QuadPrecDType())
2114+
2115+
assert result == finfo.smallest_subnormal, \
2116+
f"nextafter(0.0, 1.0) should equal smallest_subnormal, got {result} vs {finfo.smallest_subnormal}"
2117+
2118+
# Verify it's positive and very small
2119+
assert result > zero, "nextafter(0.0, 1.0) should be positive"
2120+
2121+
# Moving back towards zero should give us zero
2122+
result_back = np.nextafter(result, zero)
2123+
assert result_back == zero, f"nextafter(smallest_subnormal, 0.0) should be 0.0, got {result_back}"
2124+
2125+
def test_negative_zero(self):
2126+
"""Test nextafter with negative zero"""
2127+
neg_zero = QuadPrecision(-0.0)
2128+
pos_zero = QuadPrecision(0.0)
2129+
one = QuadPrecision(1.0)
2130+
neg_one = QuadPrecision(-1.0)
2131+
2132+
finfo = np.finfo(QuadPrecDType())
2133+
2134+
# nextafter(-0.0, 1.0) should return smallest positive subnormal
2135+
result = np.nextafter(neg_zero, one)
2136+
assert result == finfo.smallest_subnormal, \
2137+
f"nextafter(-0.0, 1.0) should be smallest_subnormal, got {result}"
2138+
assert result > pos_zero, "Result should be positive"
2139+
2140+
# nextafter(+0.0, -1.0) should return smallest negative subnormal
2141+
result_neg = np.nextafter(pos_zero, neg_one)
2142+
expected_neg_subnormal = -finfo.smallest_subnormal
2143+
assert result_neg == expected_neg_subnormal, \
2144+
f"nextafter(+0.0, -1.0) should be -smallest_subnormal, got {result_neg}"
2145+
assert result_neg < pos_zero, "Result should be negative"
2146+
2147+
def test_infinity_cases(self):
2148+
"""Test nextafter with infinity edge cases"""
2149+
pos_inf = QuadPrecision(np.inf)
2150+
neg_inf = QuadPrecision(-np.inf)
2151+
one = QuadPrecision(1.0)
2152+
neg_one = QuadPrecision(-1.0)
2153+
zero = QuadPrecision(0.0)
2154+
2155+
finfo = np.finfo(QuadPrecDType())
2156+
2157+
# nextafter(+inf, finite) should return max finite value
2158+
result = np.nextafter(pos_inf, zero)
2159+
assert not np.isinf(result), "nextafter(+inf, 0) should be finite"
2160+
assert result < pos_inf, "Result should be less than +inf"
2161+
assert result == finfo.max, f"nextafter(+inf, 0) should be max, got {result} vs {finfo.max}"
2162+
2163+
# nextafter(-inf, finite) should return -max (most negative finite)
2164+
result_neg = np.nextafter(neg_inf, zero)
2165+
assert not np.isinf(result_neg), "nextafter(-inf, 0) should be finite"
2166+
assert result_neg > neg_inf, "Result should be greater than -inf"
2167+
assert result_neg == -finfo.max, f"nextafter(-inf, 0) should be -max, got {result_neg}"
2168+
2169+
# Verify symmetry: nextafter(result, +inf) should give us +inf back
2170+
back_to_inf = np.nextafter(result, pos_inf)
2171+
assert back_to_inf == pos_inf, "nextafter(max_finite, +inf) should be +inf"
2172+
2173+
# nextafter(+inf, +inf) should return +inf
2174+
result_inf = np.nextafter(pos_inf, pos_inf)
2175+
assert result_inf == pos_inf, "nextafter(+inf, +inf) should be +inf"
2176+
2177+
# nextafter(-inf, -inf) should return -inf
2178+
result_neg_inf = np.nextafter(neg_inf, neg_inf)
2179+
assert result_neg_inf == neg_inf, "nextafter(-inf, -inf) should be -inf"
2180+
2181+
def test_max_to_infinity(self):
2182+
"""Test nextafter from max finite value to infinity"""
2183+
finfo = np.finfo(QuadPrecDType())
2184+
max_val = finfo.max
2185+
pos_inf = QuadPrecision(np.inf)
2186+
neg_inf = QuadPrecision(-np.inf)
2187+
2188+
# nextafter(max_finite, +inf) should return +inf
2189+
result = np.nextafter(max_val, pos_inf)
2190+
assert np.isinf(result), f"nextafter(max, +inf) should be inf, got {result}"
2191+
assert result > max_val, "Result should be greater than max"
2192+
assert result == pos_inf, "Result should be +inf"
2193+
2194+
# nextafter(-max_finite, -inf) should return -inf
2195+
neg_max_val = -max_val
2196+
result_neg = np.nextafter(neg_max_val, neg_inf)
2197+
assert np.isinf(result_neg), f"nextafter(-max, -inf) should be -inf, got {result_neg}"
2198+
assert result_neg < neg_max_val, "Result should be less than -max"
2199+
assert result_neg == neg_inf, "Result should be -inf"
2200+
2201+
def test_near_max(self):
2202+
"""Test nextafter near maximum finite value"""
2203+
finfo = np.finfo(QuadPrecDType())
2204+
max_val = finfo.max
2205+
zero = QuadPrecision(0.0)
2206+
pos_inf = QuadPrecision(np.inf)
2207+
2208+
# nextafter(max, 0) should return a value less than max
2209+
result = np.nextafter(max_val, zero)
2210+
assert result < max_val, "nextafter(max, 0) should be less than max"
2211+
assert not np.isinf(result), "Result should be finite"
2212+
2213+
# The difference should be one ULP at that scale
2214+
# Moving back should give us max again
2215+
result_back = np.nextafter(result, pos_inf)
2216+
assert result_back == max_val, f"Moving back should return max, got {result_back}"
2217+
2218+
def test_symmetry(self):
2219+
"""Test symmetry properties of nextafter"""
2220+
values = [0.0, 1.0, -1.0, 1e10, -1e10, 1e-10, -1e-10]
2221+
2222+
for val in values:
2223+
q_val = QuadPrecision(val)
2224+
2225+
# nextafter(x, +direction) then nextafter(result, x) should return x
2226+
if not np.isinf(val):
2227+
result_up = np.nextafter(q_val, QuadPrecision(np.inf))
2228+
result_back = np.nextafter(result_up, q_val)
2229+
assert result_back == q_val, \
2230+
f"Symmetry failed for {val}: nextafter then back should return original"
2231+
2232+
# Same for down direction
2233+
result_down = np.nextafter(q_val, QuadPrecision(-np.inf))
2234+
result_back_down = np.nextafter(result_down, q_val)
2235+
assert result_back_down == q_val, \
2236+
f"Symmetry failed for {val}: nextafter down then back should return original"
2237+
2238+
def test_direction(self):
2239+
"""Test that nextafter moves in the correct direction"""
2240+
test_cases = [
2241+
(1.0, 2.0, "greater"), # towards larger
2242+
(2.0, 1.0, "less"), # towards smaller
2243+
(-1.0, -2.0, "less"), # towards more negative
2244+
(-2.0, -1.0, "greater"), # towards less negative
2245+
(1.0, np.inf, "greater"), # towards +inf
2246+
(1.0, -np.inf, "less"), # towards -inf
2247+
]
2248+
2249+
for x, y, expected_dir in test_cases:
2250+
q_x = QuadPrecision(x)
2251+
q_y = QuadPrecision(y)
2252+
result = np.nextafter(q_x, q_y)
2253+
2254+
if expected_dir == "greater":
2255+
assert result > q_x, f"nextafter({x}, {y}) should be > {x}, got {result}"
2256+
elif expected_dir == "less":
2257+
assert result < q_x, f"nextafter({x}, {y}) should be < {x}, got {result}"
2258+

0 commit comments

Comments
 (0)