Skip to content

Commit a09c18e

Browse files
committed
spacing impl
1 parent e325053 commit a09c18e

File tree

4 files changed

+247
-2
lines changed

4 files changed

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

0 commit comments

Comments
 (0)