Skip to content

Commit effb751

Browse files
authored
Merge pull request #96 from SwayamInSync/mod-fix
Fixing `mod` calculation to use `fmod` path
2 parents 8267bf9 + 424252c commit effb751

2 files changed

Lines changed: 95 additions & 17 deletions

File tree

src/include/ops.hpp

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,11 @@ quad_pow(const Sleef_quad *a, const Sleef_quad *b)
612612
inline Sleef_quad
613613
quad_mod(const Sleef_quad *a, const Sleef_quad *b)
614614
{
615-
// division by zero
615+
// Division by zero -> NaN. IEEE 754 leaves the sign of a NaN unspecified,
616+
// and libc fmod's NaN payload varies by platform (e.g. x86 glibc returns
617+
// signbit=1, other libms may return signbit=0). We return our canonical
618+
// NaN unconditionally; callers comparing against numpy should use isnan,
619+
// not signbit, for this branch.
616620
if (Sleef_icmpeqq1(*b, QUAD_PRECISION_ZERO)) {
617621
return QUAD_PRECISION_NAN;
618622
}
@@ -640,22 +644,27 @@ quad_mod(const Sleef_quad *a, const Sleef_quad *b)
640644
// return a if sign_a == sign_b
641645
return (sign_a == sign_b) ? *a : *b;
642646
}
647+
648+
// In floor(a/b) the a/b gives the rounded quotient, off by at most 0.5ULP.
649+
// But "0.5 ulp of the quotient" can correspond to a huge absolute error when the quotient itself is huge.
650+
// hence, taking the fmod path
651+
Sleef_quad result = Sleef_fmodq1(*a, *b);
652+
// if result != 0 and sign(result) != sign(b)
653+
if (!Sleef_icmpeqq1(result, QUAD_PRECISION_ZERO) && (quad_signbit(&result) != quad_signbit(b)))
654+
{
655+
/* Interesting thing, as per "Sterbenz lemma" (https://en.wikipedia.org/wiki/Sterbenz_lemma):
656+
|b|/2 ≤ |result| < |b| => result is exact, and thus we can add b without losing precision
657+
|result| < |b|/2 => inexact but bounded by 0.5 ulp
643658
644-
// NumPy mod formula: a % b = a - floor(a/b) * b
645-
Sleef_quad quotient = Sleef_divq1_u05(*a, *b);
646-
Sleef_quad floored = Sleef_floorq1(quotient);
647-
Sleef_quad product = Sleef_mulq1_u05(floored, *b);
648-
Sleef_quad result = Sleef_subq1_u05(*a, product);
659+
In both the cases the final output will always within max of 0.5 ulp error (as documented by Sleef_addq1_u05) hence we will be in safe range.
660+
*/
661+
result = Sleef_addq1_u05(result, *b);
662+
}
649663

650664
// Handle zero result sign: when result is exactly zero,
651665
// it should have the same sign as the divisor (NumPy convention)
652666
if (Sleef_icmpeqq1(result, QUAD_PRECISION_ZERO)) {
653-
if (Sleef_icmpltq1(*b, QUAD_PRECISION_ZERO)) {
654-
return Sleef_negq1(QUAD_PRECISION_ZERO); // -0.0
655-
}
656-
else {
657-
return QUAD_PRECISION_ZERO; // +0.0
658-
}
667+
return Sleef_copysignq1(QUAD_PRECISION_ZERO, *b);
659668
}
660669

661670
return result;
@@ -669,7 +678,11 @@ quad_fmod(const Sleef_quad *a, const Sleef_quad *b)
669678
return Sleef_iunordq1(*a, *a) ? *a : *b;
670679
}
671680

672-
// Division by zero -> NaN
681+
// Division by zero -> NaN. IEEE 754 leaves the sign of a NaN unspecified,
682+
// and libc fmod's NaN payload varies by platform (e.g. x86 glibc returns
683+
// signbit=1, other libms may return signbit=0). We return our canonical
684+
// NaN unconditionally; callers comparing against numpy should use isnan,
685+
// not signbit, for this branch.
673686
if (Sleef_icmpeqq1(*b, QUAD_PRECISION_ZERO)) {
674687
return QUAD_PRECISION_NAN;
675688
}
@@ -1255,12 +1268,13 @@ ld_mod(const long double *a, const long double *b)
12551268
return (sign_a == sign_b) ? *a : *b;
12561269
}
12571270

1258-
long double quotient = (*a) / (*b);
1259-
long double floored = floorl(quotient);
1260-
long double result = (*a) - floored * (*b);
1271+
long double result = fmodl(*a, *b);
1272+
if (result != 0.0L && signbit(result) != signbit(*b)) {
1273+
result += *b;
1274+
}
12611275

12621276
if (result == 0.0L) {
1263-
return (*b < 0.0L) ? -0.0L : 0.0L;
1277+
return copysignl(0.0L, *b);
12641278
}
12651279

12661280
return result;

tests/test_quaddtype.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2731,6 +2731,70 @@ def test_mod(a, b, backend, op):
27312731
assert result_negative == numpy_negative, f"Sign mismatch for {a} % {b}: quad={result_negative}, numpy={numpy_negative}"
27322732

27332733

2734+
@pytest.mark.parametrize("op", [np.mod, np.remainder])
2735+
@pytest.mark.parametrize("a_func,a_arg,b_str", [
2736+
# Regression: huge dividend, finite divisor. cosh(-11357.2...) is
2737+
# ~1.19e+4932, so |a/b| is enormous. The old `a - floor(a/b)*b` formula
2738+
# lost all precision here because 0.5 ulp of the quotient corresponds
2739+
# to a huge absolute error. The fmod-based path avoids this.
2740+
("cosh", "-11357.216553474703894801348310092223",
2741+
"-1.7976931345860068e+308"),
2742+
# Same with mismatched signs — exercises the Python-convention sign
2743+
# adjustment on a huge result.
2744+
("cosh", "-11357.216553474703894801348310092223",
2745+
"1.7976931345860068e+308"),
2746+
# Large quotient, small divisor.
2747+
("exp", "1000", "3.14159265358979323846264338327950288"),
2748+
])
2749+
def test_mod_high_precision_sleef(a_func, a_arg, b_str, op):
2750+
"""Sleef backend: verified against mpmath at binary128 precision.
2751+
2752+
These inputs lie far outside f64 range, so the standard test_mod path
2753+
cannot exercise them — mpmath is the only viable ground truth.
2754+
"""
2755+
mp.prec = 113
2756+
mp_a = getattr(mp, a_func)(mp.mpf(a_arg))
2757+
mp_b = mp.mpf(b_str)
2758+
# mp.fmod is Python-style (sign of divisor), matching np.mod.
2759+
mp_result = mp.fmod(mp_a, mp_b)
2760+
expected = QuadPrecision(mp.nstr(mp_result, 36))
2761+
2762+
quad_a = getattr(np, a_func)(QuadPrecision(a_arg))
2763+
quad_b = QuadPrecision(b_str)
2764+
quad_result = op(quad_a, quad_b)
2765+
2766+
assert np.isclose(quad_result, expected, rtol=1e-34, atol=0), (
2767+
f"sleef mismatch for {op.__name__}({a_func}({a_arg}), {b_str}): "
2768+
f"quad={quad_result}, expected={expected}"
2769+
)
2770+
2771+
2772+
@pytest.mark.parametrize("op", [np.mod, np.remainder])
2773+
@pytest.mark.parametrize("a_func,a_arg,b_str", [
2774+
("exp", "700", "3.14159265358979323846264338327950288"),
2775+
])
2776+
def test_mod_high_precision_longdouble(a_func, a_arg, b_str, op):
2777+
"""Longdouble backend: cross-checked against numpy's np.longdouble.
2778+
2779+
The backend stores a C `long double`, the same type as np.longdouble,
2780+
so np.mod on np.longdouble inputs is a direct same-precision reference
2781+
and the result should match bit-for-bit.
2782+
"""
2783+
np_a = getattr(np, a_func)(np.longdouble(a_arg))
2784+
np_b = np.longdouble(b_str)
2785+
expected = op(np_a, np_b)
2786+
2787+
quad_a = getattr(np, a_func)(QuadPrecision(a_arg, backend="longdouble"))
2788+
quad_b = QuadPrecision(b_str, backend="longdouble")
2789+
quad_result = op(quad_a, quad_b)
2790+
2791+
# np.longdouble(QuadPrecision) is a lossless cast (same underlying type).
2792+
assert np.longdouble(quad_result) == expected, (
2793+
f"longdouble mismatch for {op.__name__}({a_func}({a_arg}), {b_str}): "
2794+
f"quad={np.longdouble(quad_result)!r}, expected={expected!r}"
2795+
)
2796+
2797+
27342798
@pytest.mark.parametrize("backend", ["sleef", "longdouble"])
27352799
@pytest.mark.parametrize("a,b", [
27362800
# Basic cases - positive/positive

0 commit comments

Comments
 (0)