Skip to content

Commit 59cf5e5

Browse files
authored
Merge pull request #191 from SwayamInSync/spacing
2 parents 50f3fd3 + 7d71a5c commit 59cf5e5

File tree

4 files changed

+275
-2
lines changed

4 files changed

+275
-2
lines changed

quaddtype/numpy_quaddtype/src/ops.hpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,43 @@ quad_nextafter(const Sleef_quad *x, const Sleef_quad *y)
10081008
return quad_set_words64(hx, lx);
10091009
}
10101010

1011+
static inline Sleef_quad
1012+
quad_spacing(const Sleef_quad *x)
1013+
{
1014+
// spacing(x) returns the distance between x and the next representable value
1015+
// The result has the SAME SIGN as x (NumPy convention)
1016+
// For x >= 0: spacing(x) = nextafter(x, +inf) - x
1017+
// For x < 0: spacing(x) = nextafter(x, -inf) - x (negative result)
1018+
1019+
// Handle NaN
1020+
if (Sleef_iunordq1(*x, *x)) {
1021+
return *x; // NaN
1022+
}
1023+
1024+
// Handle infinity -> NaN (numpy convention)
1025+
if (quad_isinf(x)) {
1026+
return QUAD_NAN;
1027+
}
1028+
1029+
// Determine direction based on sign of x
1030+
Sleef_quad direction;
1031+
if (Sleef_icmpltq1(*x, QUAD_ZERO)) {
1032+
// Negative: move toward -inf
1033+
direction = Sleef_negq1(QUAD_POS_INF);
1034+
} else {
1035+
// Positive or zero: move toward +inf
1036+
direction = QUAD_POS_INF;
1037+
}
1038+
1039+
// Compute nextafter(x, direction)
1040+
Sleef_quad next = quad_nextafter(x, &direction);
1041+
1042+
// spacing = next - x (preserves sign)
1043+
Sleef_quad result = Sleef_subq1_u05(next, *x);
1044+
1045+
return result;
1046+
}
1047+
10111048
// Binary long double operations
10121049
typedef long double (*binary_op_longdouble_def)(const long double *, const long double *);
10131050
// Binary long double operations with 2 outputs (for divmod, modf, frexp)
@@ -1292,6 +1329,38 @@ ld_nextafter(const long double *x1, const long double *x2)
12921329
return nextafterl(*x1, *x2);
12931330
}
12941331

1332+
static inline long double
1333+
ld_spacing(const long double *x)
1334+
{
1335+
// Handle NaN
1336+
if (isnan(*x)) {
1337+
return *x; // NaN
1338+
}
1339+
1340+
// Handle infinity -> NaN (numpy convention)
1341+
if (isinf(*x)) {
1342+
return NAN;
1343+
}
1344+
1345+
// Determine direction based on sign of x
1346+
long double direction;
1347+
if (*x < 0.0L) {
1348+
// Negative: move toward -inf
1349+
direction = -INFINITY;
1350+
} else {
1351+
// Positive or zero: move toward +inf
1352+
direction = INFINITY;
1353+
}
1354+
1355+
// Compute nextafter(x, direction)
1356+
long double next = nextafterl(*x, direction);
1357+
1358+
// spacing = next - x (preserves sign)
1359+
long double result = next - (*x);
1360+
1361+
return result;
1362+
}
1363+
12951364
// comparison quad functions
12961365
typedef npy_bool (*cmp_quad_def)(const Sleef_quad *, const Sleef_quad *);
12971366

quaddtype/numpy_quaddtype/src/umath/unary_ops.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,10 @@ init_quad_unary_ops(PyObject *numpy)
385385
if (create_quad_unary_ufunc<quad_radians, ld_radians>(numpy, "deg2rad") < 0) {
386386
return -1;
387387
}
388-
388+
if (create_quad_unary_ufunc<quad_spacing, ld_spacing>(numpy, "spacing") < 0) {
389+
return -1;
390+
}
391+
389392
// Logical operations (unary: not)
390393
if (create_quad_logical_not_ufunc<quad_logical_not, ld_logical_not>(numpy, "logical_not") < 0) {
391394
return -1;

quaddtype/release_tracker.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
| signbit |||
7979
| copysign |||
8080
| nextafter |||
81-
| spacing | | |
81+
| spacing | | |
8282
| modf | | |
8383
| ldexp | | |
8484
| frexp | | |

quaddtype/tests/test_quaddtype.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2256,3 +2256,204 @@ def test_direction(self):
22562256
elif expected_dir == "less":
22572257
assert result < q_x, f"nextafter({x}, {y}) should be < {x}, got {result}"
22582258

2259+
class TestSpacing:
2260+
"""Test cases for np.spacing function with QuadPrecision dtype"""
2261+
2262+
@pytest.mark.parametrize("x", [
2263+
np.nan, -np.nan,
2264+
np.inf, -np.inf,
2265+
])
2266+
def test_special_values_return_nan(self, x):
2267+
"""Test spacing with NaN and infinity inputs returns NaN"""
2268+
q_x = QuadPrecision(x)
2269+
result = np.spacing(q_x)
2270+
2271+
assert isinstance(result, QuadPrecision)
2272+
assert np.isnan(result), f"spacing({x}) should be NaN, got {result}"
2273+
2274+
@pytest.mark.parametrize("x", [
2275+
1.0, -1.0,
2276+
10.0, -10.0,
2277+
100.0, -100.0,
2278+
])
2279+
def test_sign_preservation(self, x):
2280+
"""Test that spacing preserves the sign of the input"""
2281+
q_x = QuadPrecision(x)
2282+
result = np.spacing(q_x)
2283+
2284+
q_zero = QuadPrecision(0)
2285+
# spacing should have the same sign as x
2286+
if x > 0:
2287+
assert result > q_zero, f"spacing({x}) should be positive, got {result}"
2288+
else:
2289+
assert result < q_zero, f"spacing({x}) should be negative, got {result}"
2290+
2291+
# Compare with numpy behavior
2292+
np_result = np.spacing(np.float64(x))
2293+
assert np.signbit(result) == np.signbit(np_result), \
2294+
f"Sign mismatch for spacing({x}): quad signbit={np.signbit(result)}, numpy signbit={np.signbit(np_result)}"
2295+
2296+
@pytest.mark.parametrize("x", [
2297+
0.0,
2298+
-0.0,
2299+
])
2300+
def test_zero(self, x):
2301+
"""Test spacing of zero returns smallest_subnormal"""
2302+
q_x = QuadPrecision(x)
2303+
result = np.spacing(q_x)
2304+
2305+
finfo = np.finfo(QuadPrecDType())
2306+
q_zero = QuadPrecision(0)
2307+
2308+
# spacing(±0.0) should return smallest_subnormal (positive)
2309+
assert result == finfo.smallest_subnormal, \
2310+
f"spacing({x}) should be smallest_subnormal, got {result}"
2311+
assert result > q_zero, f"spacing({x}) should be positive, got {result}"
2312+
2313+
@pytest.mark.parametrize("x", [
2314+
1.0,
2315+
-1.0,
2316+
])
2317+
def test_one_and_negative_one(self, x):
2318+
"""Test spacing(±1.0) equals ±machine epsilon"""
2319+
q_x = QuadPrecision(x)
2320+
result = np.spacing(q_x)
2321+
2322+
finfo = np.finfo(QuadPrecDType())
2323+
q_zero = QuadPrecision(0)
2324+
2325+
# For binary floating point, spacing(±1.0) = ±eps
2326+
expected = finfo.eps if x > 0 else -finfo.eps
2327+
assert result == expected, \
2328+
f"spacing({x}) should equal {expected}, got {result}"
2329+
2330+
if x > 0:
2331+
assert result > q_zero, "spacing(1.0) should be positive"
2332+
else:
2333+
assert result < q_zero, "spacing(-1.0) should be negative"
2334+
2335+
@pytest.mark.parametrize("x", [
2336+
1.0, -1.0,
2337+
2.0, -2.0,
2338+
10.0, -10.0,
2339+
100.0, -100.0,
2340+
1e10, -1e10,
2341+
1e-10, -1e-10,
2342+
0.5, -0.5,
2343+
0.25, -0.25,
2344+
])
2345+
def test_spacing_is_non_zero(self, x):
2346+
"""Test that spacing always has positive magnitude"""
2347+
q_x = QuadPrecision(x)
2348+
result = np.spacing(q_x)
2349+
2350+
q_zero = QuadPrecision(0)
2351+
# The absolute value should be positive
2352+
abs_result = np.abs(result)
2353+
assert abs_result > q_zero, f"|spacing({x})| should be positive, got {abs_result}"
2354+
2355+
def test_smallest_subnormal(self):
2356+
"""Test spacing at smallest subnormal value"""
2357+
finfo = np.finfo(QuadPrecDType())
2358+
smallest = finfo.smallest_subnormal
2359+
2360+
result = np.spacing(smallest)
2361+
2362+
q_zero = QuadPrecision(0)
2363+
# spacing(smallest_subnormal) should be smallest_subnormal itself
2364+
# (it's the minimum spacing in the subnormal range)
2365+
assert result == smallest, \
2366+
f"spacing(smallest_subnormal) should be smallest_subnormal, got {result}"
2367+
assert result > q_zero, "Result should be positive"
2368+
2369+
@pytest.mark.parametrize("x", [
2370+
1.5, -1.5,
2371+
3.7, -3.7,
2372+
42.0, -42.0,
2373+
1e8, -1e8,
2374+
])
2375+
def test_finite_values(self, x):
2376+
"""Test spacing on various finite values"""
2377+
q_x = QuadPrecision(x)
2378+
result = np.spacing(q_x)
2379+
2380+
q_zero = QuadPrecision(0)
2381+
# Result should be finite
2382+
assert np.isfinite(result), \
2383+
f"spacing({x}) should be finite, got {result}"
2384+
2385+
# Result should be non-zero
2386+
assert result != q_zero, \
2387+
f"spacing({x}) should be non-zero, got {result}"
2388+
2389+
# Result should have same sign as input
2390+
assert np.signbit(result) == np.signbit(q_x), \
2391+
f"spacing({x}) should have same sign as {x}"
2392+
2393+
def test_array_spacing(self):
2394+
"""Test spacing on an array of QuadPrecision values"""
2395+
values = [1.0, -1.0, 2.0, -2.0, 0.0, 10.0, -10.0]
2396+
q_array = np.array([QuadPrecision(v) for v in values])
2397+
2398+
result = np.spacing(q_array)
2399+
2400+
q_zero = QuadPrecision(0)
2401+
# Check each result
2402+
for i, val in enumerate(values):
2403+
q_val = QuadPrecision(val)
2404+
if val != 0:
2405+
assert np.signbit(result[i]) == np.signbit(q_val), \
2406+
f"Sign mismatch for spacing({val})"
2407+
else:
2408+
assert result[i] > q_zero, \
2409+
f"spacing(0) should be positive, got {result[i]}"
2410+
2411+
@pytest.mark.parametrize("x", [
2412+
1e-100, -1e-100,
2413+
1e-200, -1e-200,
2414+
])
2415+
def test_subnormal_range(self, x):
2416+
"""Test spacing in subnormal range"""
2417+
q_x = QuadPrecision(x)
2418+
result = np.spacing(q_x)
2419+
2420+
finfo = np.finfo(QuadPrecDType())
2421+
2422+
# In subnormal range, spacing should be smallest_subnormal
2423+
# or at least very small
2424+
assert np.abs(result) >= finfo.smallest_subnormal, \
2425+
f"spacing({x}) should be >= smallest_subnormal"
2426+
2427+
q_zero = QuadPrecision(0)
2428+
# Sign should match (but for very small subnormals, spacing might underflow to zero)
2429+
if result != q_zero:
2430+
assert np.signbit(result) == np.signbit(q_x), \
2431+
f"spacing({x}) should have same sign as {x}"
2432+
2433+
def test_smallest_normal_spacing(self):
2434+
"""Test spacing for smallest normal value and 2*smallest normal"""
2435+
finfo = np.finfo(QuadPrecDType())
2436+
smallest_normal = finfo.smallest_normal
2437+
2438+
# Test spacing at smallest normal value
2439+
result1 = np.spacing(smallest_normal)
2440+
2441+
# Test spacing at 2 * smallest normal value
2442+
two_smallest_normal = 2 * smallest_normal
2443+
result2 = np.spacing(two_smallest_normal)
2444+
2445+
q_zero = QuadPrecision("0")
2446+
2447+
# spacing(smallest_normal) should be smallest_subnormal
2448+
# (the spacing at the boundary between subnormal and normal range)
2449+
expected1 = finfo.smallest_subnormal
2450+
assert result1 == expected1, \
2451+
f"spacing(smallest_normal) should be {expected1}, got {result1}"
2452+
assert result1 > q_zero, "Result should be positive"
2453+
2454+
# The scaling relationship: spacing(2*x) = 2*spacing(x) for normal numbers
2455+
expected2 = 2 * finfo.smallest_subnormal
2456+
assert result2 == expected2, \
2457+
f"spacing(2*smallest_normal) should be {expected2}, got {result2}"
2458+
assert result2 > q_zero, "Result should be positive"
2459+

0 commit comments

Comments
 (0)