Skip to content

Fixing mod's behaviour to numpy from IEEE #125

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 83 additions & 17 deletions quaddtype/numpy_quaddtype/src/ops.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#define QUAD_ZERO sleef_q(+0x0000000000000LL, 0x0000000000000000ULL, -16383)
#define QUAD_ONE sleef_q(+0x1000000000000LL, 0x0000000000000000ULL, 0)
#define QUAD_POS_INF sleef_q(+0x1000000000000LL, 0x0000000000000000ULL, 16384)
#define QUAD_NAN sleef_q(+0x1ffffffffffffLL, 0xffffffffffffffffULL, 16384)

// Unary Quad Operations
typedef Sleef_quad (*unary_op_quad_def)(const Sleef_quad *);
Expand Down Expand Up @@ -174,9 +175,12 @@ ld_absolute(const long double *op)
static inline long double
ld_sign(const long double *op)
{
if (*op < 0.0) return -1.0;
if (*op == 0.0) return 0.0;
if (*op > 0.0) return 1.0;
if (*op < 0.0)
return -1.0;
if (*op == 0.0)
return 0.0;
if (*op > 0.0)
return 1.0;
// sign(x=NaN) = x
return *op;
}
Expand Down Expand Up @@ -391,39 +395,80 @@ quad_pow(const Sleef_quad *a, const Sleef_quad *b)
static inline Sleef_quad
quad_mod(const Sleef_quad *a, const Sleef_quad *b)
{
return Sleef_fmodq1(*a, *b);
// division by zero
if (Sleef_icmpeqq1(*b, QUAD_ZERO)) {
return QUAD_NAN;
}

// NaN inputs
if (Sleef_iunordq1(*a, *b)) {
return Sleef_iunordq1(*a, *a) ? *a : *b; // Return the NaN
}

// infinity dividend -> NaN
if (quad_isinf(a)) {
return QUAD_NAN;
}

// finite % inf
if (quad_isfinite(a) && quad_isinf(b)) {
int sign_a = quad_signbit(a);
int sign_b = quad_signbit(b);

// return a if sign_a == sign_b
return (sign_a == sign_b) ? *a : *b;
}

// NumPy mod formula: a % b = a - floor(a/b) * b
Sleef_quad quotient = Sleef_divq1_u05(*a, *b);
Sleef_quad floored = Sleef_floorq1(quotient);
Sleef_quad product = Sleef_mulq1_u05(floored, *b);
Sleef_quad result = Sleef_subq1_u05(*a, product);

// Handle zero result sign: when result is exactly zero,
// it should have the same sign as the divisor (NumPy convention)
if (Sleef_icmpeqq1(result, QUAD_ZERO)) {
if (Sleef_icmpltq1(*b, QUAD_ZERO)) {
return Sleef_negq1(QUAD_ZERO); // -0.0
}
else {
return QUAD_ZERO; // +0.0
}
}

return result;
}

static inline Sleef_quad
quad_minimum(const Sleef_quad *in1, const Sleef_quad *in2)
{
return Sleef_iunordq1(*in1, *in2) ? (
Sleef_iunordq1(*in1, *in1) ? *in1 : *in2
) : Sleef_icmpleq1(*in1, *in2) ? *in1 : *in2;
return Sleef_iunordq1(*in1, *in2) ? (Sleef_iunordq1(*in1, *in1) ? *in1 : *in2)
: Sleef_icmpleq1(*in1, *in2) ? *in1
: *in2;
}

static inline Sleef_quad
quad_maximum(const Sleef_quad *in1, const Sleef_quad *in2)
{
return Sleef_iunordq1(*in1, *in2) ? (
Sleef_iunordq1(*in1, *in1) ? *in1 : *in2
) : Sleef_icmpgeq1(*in1, *in2) ? *in1 : *in2;
return Sleef_iunordq1(*in1, *in2) ? (Sleef_iunordq1(*in1, *in1) ? *in1 : *in2)
: Sleef_icmpgeq1(*in1, *in2) ? *in1
: *in2;
}

static inline Sleef_quad
quad_fmin(const Sleef_quad *in1, const Sleef_quad *in2)
{
return Sleef_iunordq1(*in1, *in2) ? (
Sleef_iunordq1(*in2, *in2) ? *in1 : *in2
) : Sleef_icmpleq1(*in1, *in2) ? *in1 : *in2;
return Sleef_iunordq1(*in1, *in2) ? (Sleef_iunordq1(*in2, *in2) ? *in1 : *in2)
: Sleef_icmpleq1(*in1, *in2) ? *in1
: *in2;
}

static inline Sleef_quad
quad_fmax(const Sleef_quad *in1, const Sleef_quad *in2)
{
return Sleef_iunordq1(*in1, *in2) ? (
Sleef_iunordq1(*in2, *in2) ? *in1 : *in2
) : Sleef_icmpgeq1(*in1, *in2) ? *in1 : *in2;
return Sleef_iunordq1(*in1, *in2) ? (Sleef_iunordq1(*in2, *in2) ? *in1 : *in2)
: Sleef_icmpgeq1(*in1, *in2) ? *in1
: *in2;
}

static inline Sleef_quad
Expand Down Expand Up @@ -474,7 +519,28 @@ ld_pow(const long double *a, const long double *b)
static inline long double
ld_mod(const long double *a, const long double *b)
{
return fmodl(*a, *b);
if (*b == 0.0L)
return NAN;
if (isnan(*a) || isnan(*b))
return isnan(*a) ? *a : *b;
if (isinf(*a))
return NAN;

if (isfinite(*a) && isinf(*b)) {
int sign_a = signbit(*a);
int sign_b = signbit(*b);
return (sign_a == sign_b) ? *a : *b;
}

long double quotient = (*a) / (*b);
long double floored = floorl(quotient);
long double result = (*a) - floored * (*b);

if (result == 0.0L) {
return (*b < 0.0L) ? -0.0L : 0.0L;
}

return result;
}

static inline long double
Expand Down
5 changes: 3 additions & 2 deletions quaddtype/release_tracker.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Plan for `numpy-quaddtype` v1.0.0

- [ ] High-Endian System support
- [ ] Complete Documentation

Expand All @@ -17,8 +18,8 @@
| positive | ✅ | ✅ |
| power | ✅ | ✅ |
| float_power | | |
| remainder | | |
| mod | ✅ | ❌ _Need: basic tests + edge cases (NaN/inf/-0.0/large values)_ |
| remainder | | ✅ |
| mod | ✅ | |
| fmod | | |
| divmod | | |
| absolute | ✅ | ✅ |
Expand Down
80 changes: 80 additions & 0 deletions quaddtype/tests/test_quaddtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,83 @@ def test_array_operations():
expected = np.array(
[QuadPrecision("2.0"), QuadPrecision("3.5"), QuadPrecision("5.0")])
assert all(x == y for x, y in zip(result, expected))


@pytest.mark.parametrize("backend", ["sleef", "longdouble"])
@pytest.mark.parametrize("op", [np.mod, np.remainder])
@pytest.mark.parametrize("a,b", [
# Basic cases - positive/negative combinations
(7.0, 3.0), (-7.0, 3.0), (7.0, -3.0), (-7.0, -3.0),

# Zero dividend cases
(0.0, 3.0), (-0.0, 3.0), (0.0, -3.0), (-0.0, -3.0),

# Cases that result in zero (sign testing)
(6.0, 3.0), (-6.0, 3.0), (6.0, -3.0), (-6.0, -3.0),
(1.0, 1.0), (-1.0, 1.0), (1.0, -1.0), (-1.0, -1.0),

# Fractional cases
(7.5, 2.5), (-7.5, 2.5), (7.5, -2.5), (-7.5, -2.5),
(0.75, 0.25), (-0.1, 0.3), (0.9, -1.0), (-1.1, -1.0),

# Large/small numbers
(1e10, 1e5), (-1e10, 1e5), (1e-10, 1e-5), (-1e-10, 1e-5),

# Finite % infinity cases
(5.0, float('inf')), (-5.0, float('inf')),
(5.0, float('-inf')), (-5.0, float('-inf')),
(0.0, float('inf')), (-0.0, float('-inf')),

# NaN cases (should return NaN)
(float('nan'), 3.0), (3.0, float('nan')), (float('nan'), float('nan')),

# Division by zero cases (should return NaN)
(5.0, 0.0), (-5.0, 0.0), (0.0, 0.0), (-0.0, 0.0),

# Infinity dividend cases (should return NaN)
(float('inf'), 3.0), (float('-inf'), 3.0),
(float('inf'), float('inf')), (float('-inf'), float('-inf')),
])
def test_mod(a, b, backend, op):
"""Comprehensive test for mod operation against NumPy behavior"""
if backend == "sleef":
quad_a = QuadPrecision(str(a))
quad_b = QuadPrecision(str(b))
elif backend == "longdouble":
quad_a = QuadPrecision(a, backend='longdouble')
quad_b = QuadPrecision(b, backend='longdouble')
float_a = np.float64(a)
float_b = np.float64(b)

quad_result = op(quad_a, quad_b)
numpy_result = op(float_a, float_b)

# Handle NaN cases
if np.isnan(numpy_result):
assert np.isnan(
float(quad_result)), f"Expected NaN for {a} % {b}, got {float(quad_result)}"
return

if np.isinf(numpy_result):
assert np.isinf(
float(quad_result)), f"Expected inf for {a} % {b}, got {float(quad_result)}"
assert np.sign(numpy_result) == np.sign(
float(quad_result)), f"Infinity sign mismatch for {a} % {b}"
return

np.testing.assert_allclose(float(quad_result), numpy_result, rtol=1e-10, atol=1e-15,
err_msg=f"Value mismatch for {a} % {b}")

if numpy_result == 0.0:
numpy_sign = np.signbit(numpy_result)
quad_sign = np.signbit(quad_result)
assert numpy_sign == quad_sign, f"Zero sign mismatch for {a} % {b}: numpy={numpy_sign}, quad={quad_sign}"

# Check that non-zero results have correct sign relative to divisor
if numpy_result != 0.0 and not np.isnan(b) and not np.isinf(b) and b != 0.0:
# In Python mod, non-zero result should have same sign as divisor (or be zero)
result_negative = float(quad_result) < 0
divisor_negative = b < 0
numpy_negative = numpy_result < 0

assert result_negative == numpy_negative, f"Sign mismatch for {a} % {b}: quad={result_negative}, numpy={numpy_negative}"
Loading