From bbdd72c64a5d4b601d8265cfe3b70193c9a6a47b Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Thu, 30 Oct 2025 12:13:44 +0000 Subject: [PATCH 1/9] num is losing prec --- quaddtype/numpy_quaddtype/src/scalar.c | 222 +++++++++++++++++++++++ quaddtype/tests/test_quaddtype.py | 238 ++++++++++++++++++++++++- 2 files changed, 459 insertions(+), 1 deletion(-) diff --git a/quaddtype/numpy_quaddtype/src/scalar.c b/quaddtype/numpy_quaddtype/src/scalar.c index 2be5078..04f109f 100644 --- a/quaddtype/numpy_quaddtype/src/scalar.c +++ b/quaddtype/numpy_quaddtype/src/scalar.c @@ -383,6 +383,227 @@ QuadPrecision_get_imag(QuadPrecisionObject *self, void *closure) return (PyObject *)QuadPrecision_raw_new(self->backend); } +// Method implementations for float compatibility +static PyObject * +QuadPrecision_is_integer(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored)) +{ + Sleef_quad value; + + if (self->backend == BACKEND_SLEEF) { + value = self->value.sleef_value; + } + else { + value = Sleef_cast_from_doubleq1((double)self->value.longdouble_value); + } + + // Check if value is finite (not inf or nan) + // Using the same approach as quad_isfinite: abs(x) < inf + Sleef_quad abs_value = Sleef_fabsq1(value); + Sleef_quad pos_inf = Sleef_cast_from_doubleq1(INFINITY); + int32_t is_finite = Sleef_icmpltq1(abs_value, pos_inf); + + if (!is_finite) { + Py_RETURN_FALSE; + } + + // Check if value equals its truncated version + Sleef_quad truncated = Sleef_truncq1(value); + int32_t is_equal = Sleef_icmpeqq1(value, truncated); + + if (is_equal) { + Py_RETURN_TRUE; + } + else { + Py_RETURN_FALSE; + } +} + +static PyObject * +QuadPrecision_as_integer_ratio(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored)) +{ + Sleef_quad value; + + if (self->backend == BACKEND_SLEEF) { + value = self->value.sleef_value; + } + else { + value = Sleef_cast_from_doubleq1((double)self->value.longdouble_value); + } + + // Check for infinity using: abs(x) == inf + Sleef_quad abs_value = Sleef_fabsq1(value); + Sleef_quad pos_inf = Sleef_cast_from_doubleq1(INFINITY); + int32_t is_inf = Sleef_icmpeqq1(abs_value, pos_inf); + + if (is_inf) { + PyErr_SetString(PyExc_OverflowError, "cannot convert Infinity to integer ratio"); + return NULL; + } + + // Check for NaN using: x != x (NaN property) + int32_t is_nan = !Sleef_icmpeqq1(value, value); + + if (is_nan) { + PyErr_SetString(PyExc_ValueError, "cannot convert NaN to integer ratio"); + return NULL; + } + + // Handle zero + Sleef_quad zero = Sleef_cast_from_int64q1(0); + if (Sleef_icmpeqq1(value, zero)) { + PyObject *numerator = PyLong_FromLong(0); + PyObject *denominator = PyLong_FromLong(1); + if (!numerator || !denominator) { + Py_XDECREF(numerator); + Py_XDECREF(denominator); + return NULL; + } + PyObject *result = PyTuple_Pack(2, numerator, denominator); + Py_DECREF(numerator); + Py_DECREF(denominator); + return result; + } + + // Remember the sign and work with absolute value + int is_negative = Sleef_icmpltq1(value, zero); + abs_value = Sleef_fabsq1(value); + + // Extract mantissa and exponent using frexp + // frexp returns value = mantissa * 2^exponent, where 0.5 <= |mantissa| < 1 + int exponent; + Sleef_quad mantissa = Sleef_frexpq1(abs_value, &exponent); + + // For quad precision, we have 113 bits of precision + // Scale mantissa by 2^113 to get all significant bits as an integer + const int QUAD_MANT_DIG = 113; + + // We'll build the numerator by converting the mantissa to a hex string + // and parsing it, which preserves all the precision + char hex_buffer[64]; + Sleef_snprintf(hex_buffer, sizeof(hex_buffer), "%.28Qa", mantissa); + + // Parse the hex representation to get exact mantissa bits + // The format is like "0x1.fffffp+0" or similar + // We need to extract the mantissa and exponent separately + + // Instead of using hex parsing (which is complex), let's use a different approach: + // Build the mantissa as an integer by extracting 64-bit chunks + + // Multiply mantissa by 2^113 to shift all bits into the integer part + Sleef_quad scaled = Sleef_ldexpq1(mantissa, QUAD_MANT_DIG); + + // Now extract the integer value in two 64-bit chunks + // First get the upper 64 bits + Sleef_quad two_64 = Sleef_cast_from_doubleq1(18446744073709551616.0); // 2^64 + Sleef_quad upper_part = Sleef_floorq1(Sleef_divq1_u05(scaled, two_64)); + uint64_t upper_bits = Sleef_cast_to_uint64q1(upper_part); + + // Get the lower 64 bits + Sleef_quad lower_part_quad = Sleef_subq1_u05(scaled, Sleef_mulq1_u05(upper_part, two_64)); + uint64_t lower_bits = Sleef_cast_to_uint64q1(lower_part_quad); + + // Build Python integer from the two 64-bit parts + PyObject *upper_py = PyLong_FromUnsignedLongLong(upper_bits); + if (!upper_py) { + return NULL; + } + + PyObject *shift_64 = PyLong_FromLong(64); + if (!shift_64) { + Py_DECREF(upper_py); + return NULL; + } + + PyObject *shifted_upper = PyNumber_Lshift(upper_py, shift_64); + Py_DECREF(upper_py); + Py_DECREF(shift_64); + if (!shifted_upper) { + return NULL; + } + + PyObject *lower_py = PyLong_FromUnsignedLongLong(lower_bits); + if (!lower_py) { + Py_DECREF(shifted_upper); + return NULL; + } + + PyObject *numerator = PyNumber_Add(shifted_upper, lower_py); + Py_DECREF(shifted_upper); + Py_DECREF(lower_py); + if (!numerator) { + return NULL; + } + + // Calculate the final exponent + // value = mantissa * 2^exponent = (numerator / 2^113) * 2^exponent + // value = numerator * 2^(exponent - 113) + int final_exponent = exponent - QUAD_MANT_DIG; + + PyObject *denominator; + if (final_exponent >= 0) { + // Shift numerator left + PyObject *shift = PyLong_FromLong(final_exponent); + if (!shift) { + Py_DECREF(numerator); + return NULL; + } + PyObject *new_numerator = PyNumber_Lshift(numerator, shift); + Py_DECREF(shift); + Py_DECREF(numerator); + if (!new_numerator) { + return NULL; + } + numerator = new_numerator; + denominator = PyLong_FromLong(1); + } + else { + // Shift denominator left (denominator = 2^(-final_exponent)) + PyObject *shift = PyLong_FromLong(-final_exponent); + if (!shift) { + Py_DECREF(numerator); + return NULL; + } + PyObject *one = PyLong_FromLong(1); + if (!one) { + Py_DECREF(shift); + Py_DECREF(numerator); + return NULL; + } + denominator = PyNumber_Lshift(one, shift); + Py_DECREF(one); + Py_DECREF(shift); + if (!denominator) { + Py_DECREF(numerator); + return NULL; + } + } + + // Apply sign + if (is_negative) { + PyObject *new_numerator = PyNumber_Negative(numerator); + Py_DECREF(numerator); + if (!new_numerator) { + Py_DECREF(denominator); + return NULL; + } + numerator = new_numerator; + } + + // Create and return the tuple + PyObject *result = PyTuple_Pack(2, numerator, denominator); + Py_DECREF(numerator); + Py_DECREF(denominator); + return result; +} + +static PyMethodDef QuadPrecision_methods[] = { + {"is_integer", (PyCFunction)QuadPrecision_is_integer, METH_NOARGS, + "Return True if the value is an integer."}, + {"as_integer_ratio", (PyCFunction)QuadPrecision_as_integer_ratio, METH_NOARGS, + "Return a pair of integers whose ratio is exactly equal to the original value."}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + static PyGetSetDef QuadPrecision_getset[] = { {"real", (getter)QuadPrecision_get_real, NULL, "Real part of the scalar", NULL}, {"imag", (getter)QuadPrecision_get_imag, NULL, "Imaginary part of the scalar (always 0 for real types)", NULL}, @@ -400,6 +621,7 @@ PyTypeObject QuadPrecision_Type = { .tp_as_number = &quad_as_scalar, .tp_as_buffer = &QuadPrecision_as_buffer, .tp_richcompare = (richcmpfunc)quad_richcompare, + .tp_methods = QuadPrecision_methods, .tp_getset = QuadPrecision_getset, }; diff --git a/quaddtype/tests/test_quaddtype.py b/quaddtype/tests/test_quaddtype.py index d657d8d..ed92e2a 100644 --- a/quaddtype/tests/test_quaddtype.py +++ b/quaddtype/tests/test_quaddtype.py @@ -3507,4 +3507,240 @@ def test_fromfile_single_value(self): expected = np.array([42.0], dtype=QuadPrecDType(backend='sleef')) np.testing.assert_array_equal(result, expected) finally: - os.unlink(fname) \ No newline at end of file + os.unlink(fname) + + +class TestFloatCompatibilityMethods: + """Test suite for float compatibility methods: is_integer() and as_integer_ratio().""" + + def test_is_integer_true_cases(self): + """Test is_integer() returns True for integer values.""" + # Positive integers + assert QuadPrecision("1.0").is_integer() + assert QuadPrecision("42.0").is_integer() + assert QuadPrecision("1000.0").is_integer() + + # Negative integers + assert QuadPrecision("-1.0").is_integer() + assert QuadPrecision("-42.0").is_integer() + + # Zero + assert QuadPrecision("0.0").is_integer() + assert QuadPrecision("-0.0").is_integer() + + # Large integers + assert QuadPrecision("1e20").is_integer() + assert QuadPrecision("123456789012345678901234567890").is_integer() + + def test_is_integer_false_cases(self): + """Test is_integer() returns False for non-integer values.""" + # Simple fractional values + assert not QuadPrecision("1.5").is_integer() + assert not QuadPrecision("3.14").is_integer() + assert not QuadPrecision("-2.5").is_integer() + + # Small fractional values + assert not QuadPrecision("0.1").is_integer() + assert not QuadPrecision("0.0001").is_integer() + + # Values close to integers but not exact + assert not QuadPrecision("1.0000000000001").is_integer() + assert not QuadPrecision("0.9999999999999").is_integer() + + def test_is_integer_special_values(self): + """Test is_integer() with special values.""" + # Infinity should return False + inf = QuadPrecision("inf") + assert not inf.is_integer() + + neg_inf = QuadPrecision("-inf") + assert not neg_inf.is_integer() + + # NaN should return False + nan = QuadPrecision("nan") + assert not nan.is_integer() + + def test_is_integer_compatibility_with_float(self): + """Test is_integer() matches behavior of Python's float.""" + test_values = ["0.0", "1.0", "1.5", "-3.0", "-3.7", "42.0"] + + for val_str in test_values: + quad_val = QuadPrecision(val_str) + float_val = float(val_str) + + assert quad_val.is_integer() == float_val.is_integer(), \ + f"Mismatch for {val_str}: quad={quad_val.is_integer()}, float={float_val.is_integer()}" + + def test_as_integer_ratio_simple_cases(self): + """Test as_integer_ratio() for simple values.""" + # Integer values + num, denom = QuadPrecision("1.0").as_integer_ratio() + assert num == 1 and denom == 1 + + num, denom = QuadPrecision("42.0").as_integer_ratio() + assert num == 42 and denom == 1 + + num, denom = QuadPrecision("-5.0").as_integer_ratio() + assert num == -5 and denom == 1 + + # Zero + num, denom = QuadPrecision("0.0").as_integer_ratio() + assert num == 0 and denom == 1 + + def test_as_integer_ratio_fractional_values(self): + """Test as_integer_ratio() for fractional values.""" + # 0.5 = 1/2 + num, denom = QuadPrecision("0.5").as_integer_ratio() + assert num / denom == 0.5 + assert denom > 0 # Denominator should always be positive + + # 0.25 = 1/4 + num, denom = QuadPrecision("0.25").as_integer_ratio() + assert num / denom == 0.25 + + # 1.5 = 3/2 + num, denom = QuadPrecision("1.5").as_integer_ratio() + assert num / denom == 1.5 + + # -2.5 = -5/2 + num, denom = QuadPrecision("-2.5").as_integer_ratio() + assert num / denom == -2.5 + + def test_as_integer_ratio_reconstruction(self): + """Test that as_integer_ratio() can reconstruct the original value.""" + test_values = ["3.14", "0.1", "1.414213562373095", "2.718281828459045", + "-1.23456789", "1000.001", "0.0001"] + + for val_str in test_values: + quad_val = QuadPrecision(val_str) + num, denom = quad_val.as_integer_ratio() + + # Reconstruct the value + reconstructed = QuadPrecision(num) / QuadPrecision(denom) + + # Should be very close (within quad precision limits) + assert reconstructed == quad_val, \ + f"Failed to reconstruct {val_str}: got {reconstructed} from {num}/{denom}" + + def test_as_integer_ratio_return_types(self): + """Test that as_integer_ratio() returns Python ints.""" + num, denom = QuadPrecision("3.14").as_integer_ratio() + + assert isinstance(num, int), f"Numerator should be int, got {type(num)}" + assert isinstance(denom, int), f"Denominator should be int, got {type(denom)}" + + def test_as_integer_ratio_denominator_positive(self): + """Test that denominator is always positive.""" + test_values = ["-1.0", "-3.14", "-0.5", "1.0", "3.14", "0.5"] + + for val_str in test_values: + num, denom = QuadPrecision(val_str).as_integer_ratio() + assert denom > 0, f"Denominator should be positive for {val_str}, got {denom}" + + def test_as_integer_ratio_infinity_raises(self): + """Test that as_integer_ratio() raises OverflowError for infinity.""" + inf = QuadPrecision("inf") + with pytest.raises(OverflowError, match="cannot convert Infinity to integer ratio"): + inf.as_integer_ratio() + + neg_inf = QuadPrecision("-inf") + with pytest.raises(OverflowError, match="cannot convert Infinity to integer ratio"): + neg_inf.as_integer_ratio() + + def test_as_integer_ratio_nan_raises(self): + """Test that as_integer_ratio() raises ValueError for NaN.""" + nan = QuadPrecision("nan") + with pytest.raises(ValueError, match="cannot convert NaN to integer ratio"): + nan.as_integer_ratio() + + def test_as_integer_ratio_compatibility_with_float(self): + """Test as_integer_ratio() matches behavior of Python's float where possible.""" + # For values that fit in float64 precision + test_values = ["1.0", "0.5", "3.14", "-2.5", "0.0"] + + for val_str in test_values: + quad_val = QuadPrecision(val_str) + float_val = float(val_str) + + quad_num, quad_denom = quad_val.as_integer_ratio() + float_num, float_denom = float_val.as_integer_ratio() + + # The ratios should be equal (though potentially in different reduced forms) + quad_ratio = quad_num / quad_denom + float_ratio = float_num / float_denom + + assert abs(quad_ratio - float_ratio) < 1e-15, \ + f"Mismatch for {val_str}: quad={quad_num}/{quad_denom}, float={float_num}/{float_denom}" + + def test_as_integer_ratio_large_values(self): + """Test as_integer_ratio() with large values.""" + # Large integer + large_val = QuadPrecision("1e20") + num, denom = large_val.as_integer_ratio() + assert num / denom == float(str(large_val)) + + # Large fractional value + large_frac = QuadPrecision("1.23e15") + num, denom = large_frac.as_integer_ratio() + reconstructed = num / denom + assert abs(reconstructed - 1.23e15) < 1e10 # Allow some tolerance for large numbers + + def test_as_integer_ratio_small_values(self): + """Test as_integer_ratio() with very small values.""" + small_val = QuadPrecision("1e-30") + num, denom = small_val.as_integer_ratio() + + # Reconstruct and verify + reconstructed = QuadPrecision(num) / QuadPrecision(denom) + assert reconstructed == small_val + + def test_methods_available_on_type(self): + """Test that methods are available on the QuadPrecision class.""" + # Check that the methods exist + assert hasattr(QuadPrecision, 'is_integer') + assert hasattr(QuadPrecision, 'as_integer_ratio') + + # Check that they're callable + assert callable(getattr(QuadPrecision, 'is_integer')) + assert callable(getattr(QuadPrecision, 'as_integer_ratio')) + + def test_methods_available_on_instance(self): + """Test that methods are available on QuadPrecision instances.""" + val = QuadPrecision("3.14") + + # Check that the methods exist on the instance + assert hasattr(val, 'is_integer') + assert hasattr(val, 'as_integer_ratio') + + # Check that they're callable + assert callable(val.is_integer) + assert callable(val.as_integer_ratio) + + def test_is_integer_with_different_backends(self): + """Test is_integer() with both SLEEF and longdouble backends.""" + # Note: This test assumes longdouble backend is available + try: + sleef_val = QuadPrecision("3.0", backend="sleef") + assert sleef_val.is_integer() + + # Only test longdouble if it's actually 128-bit + if numpy_quaddtype.is_longdouble_128(): + ld_val = QuadPrecision("3.0", backend="longdouble") + assert ld_val.is_integer() + except Exception: + pytest.skip("Backend not available") + + def test_as_integer_ratio_with_different_backends(self): + """Test as_integer_ratio() with both SLEEF and longdouble backends.""" + try: + sleef_val = QuadPrecision("1.5", backend="sleef") + sleef_num, sleef_denom = sleef_val.as_integer_ratio() + assert sleef_num / sleef_denom == 1.5 + + # Only test longdouble if it's actually 128-bit + if numpy_quaddtype.is_longdouble_128(): + ld_val = QuadPrecision("1.5", backend="longdouble") + ld_num, ld_denom = ld_val.as_integer_ratio() + assert ld_num / ld_denom == 1.5 + except Exception: + pytest.skip("Backend not available") From cb3243d7d13aa02a5479233721680d4afc1c6e8f Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 31 Oct 2025 11:24:14 +0000 Subject: [PATCH 2/9] adding is_integer --- quaddtype/numpy_quaddtype/src/scalar.c | 180 +------------ quaddtype/tests/test_quaddtype.py | 360 ++++++++++--------------- 2 files changed, 141 insertions(+), 399 deletions(-) diff --git a/quaddtype/numpy_quaddtype/src/scalar.c b/quaddtype/numpy_quaddtype/src/scalar.c index 04f109f..e959d4b 100644 --- a/quaddtype/numpy_quaddtype/src/scalar.c +++ b/quaddtype/numpy_quaddtype/src/scalar.c @@ -393,13 +393,17 @@ QuadPrecision_is_integer(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored) value = self->value.sleef_value; } else { + // lets also tackle ld from sleef functions as well value = Sleef_cast_from_doubleq1((double)self->value.longdouble_value); } + if(Sleef_iunordq1(value, value)) { + Py_RETURN_FALSE; + } + // Check if value is finite (not inf or nan) - // Using the same approach as quad_isfinite: abs(x) < inf Sleef_quad abs_value = Sleef_fabsq1(value); - Sleef_quad pos_inf = Sleef_cast_from_doubleq1(INFINITY); + Sleef_quad pos_inf = sleef_q(+0x1000000000000LL, 0x0000000000000000ULL, 16384); int32_t is_finite = Sleef_icmpltq1(abs_value, pos_inf); if (!is_finite) { @@ -421,179 +425,7 @@ QuadPrecision_is_integer(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored) static PyObject * QuadPrecision_as_integer_ratio(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored)) { - Sleef_quad value; - - if (self->backend == BACKEND_SLEEF) { - value = self->value.sleef_value; - } - else { - value = Sleef_cast_from_doubleq1((double)self->value.longdouble_value); - } - - // Check for infinity using: abs(x) == inf - Sleef_quad abs_value = Sleef_fabsq1(value); - Sleef_quad pos_inf = Sleef_cast_from_doubleq1(INFINITY); - int32_t is_inf = Sleef_icmpeqq1(abs_value, pos_inf); - - if (is_inf) { - PyErr_SetString(PyExc_OverflowError, "cannot convert Infinity to integer ratio"); - return NULL; - } - - // Check for NaN using: x != x (NaN property) - int32_t is_nan = !Sleef_icmpeqq1(value, value); - - if (is_nan) { - PyErr_SetString(PyExc_ValueError, "cannot convert NaN to integer ratio"); - return NULL; - } - - // Handle zero - Sleef_quad zero = Sleef_cast_from_int64q1(0); - if (Sleef_icmpeqq1(value, zero)) { - PyObject *numerator = PyLong_FromLong(0); - PyObject *denominator = PyLong_FromLong(1); - if (!numerator || !denominator) { - Py_XDECREF(numerator); - Py_XDECREF(denominator); - return NULL; - } - PyObject *result = PyTuple_Pack(2, numerator, denominator); - Py_DECREF(numerator); - Py_DECREF(denominator); - return result; - } - - // Remember the sign and work with absolute value - int is_negative = Sleef_icmpltq1(value, zero); - abs_value = Sleef_fabsq1(value); - - // Extract mantissa and exponent using frexp - // frexp returns value = mantissa * 2^exponent, where 0.5 <= |mantissa| < 1 - int exponent; - Sleef_quad mantissa = Sleef_frexpq1(abs_value, &exponent); - - // For quad precision, we have 113 bits of precision - // Scale mantissa by 2^113 to get all significant bits as an integer - const int QUAD_MANT_DIG = 113; - - // We'll build the numerator by converting the mantissa to a hex string - // and parsing it, which preserves all the precision - char hex_buffer[64]; - Sleef_snprintf(hex_buffer, sizeof(hex_buffer), "%.28Qa", mantissa); - - // Parse the hex representation to get exact mantissa bits - // The format is like "0x1.fffffp+0" or similar - // We need to extract the mantissa and exponent separately - - // Instead of using hex parsing (which is complex), let's use a different approach: - // Build the mantissa as an integer by extracting 64-bit chunks - - // Multiply mantissa by 2^113 to shift all bits into the integer part - Sleef_quad scaled = Sleef_ldexpq1(mantissa, QUAD_MANT_DIG); - - // Now extract the integer value in two 64-bit chunks - // First get the upper 64 bits - Sleef_quad two_64 = Sleef_cast_from_doubleq1(18446744073709551616.0); // 2^64 - Sleef_quad upper_part = Sleef_floorq1(Sleef_divq1_u05(scaled, two_64)); - uint64_t upper_bits = Sleef_cast_to_uint64q1(upper_part); - - // Get the lower 64 bits - Sleef_quad lower_part_quad = Sleef_subq1_u05(scaled, Sleef_mulq1_u05(upper_part, two_64)); - uint64_t lower_bits = Sleef_cast_to_uint64q1(lower_part_quad); - - // Build Python integer from the two 64-bit parts - PyObject *upper_py = PyLong_FromUnsignedLongLong(upper_bits); - if (!upper_py) { - return NULL; - } - - PyObject *shift_64 = PyLong_FromLong(64); - if (!shift_64) { - Py_DECREF(upper_py); - return NULL; - } - - PyObject *shifted_upper = PyNumber_Lshift(upper_py, shift_64); - Py_DECREF(upper_py); - Py_DECREF(shift_64); - if (!shifted_upper) { - return NULL; - } - - PyObject *lower_py = PyLong_FromUnsignedLongLong(lower_bits); - if (!lower_py) { - Py_DECREF(shifted_upper); - return NULL; - } - - PyObject *numerator = PyNumber_Add(shifted_upper, lower_py); - Py_DECREF(shifted_upper); - Py_DECREF(lower_py); - if (!numerator) { - return NULL; - } - - // Calculate the final exponent - // value = mantissa * 2^exponent = (numerator / 2^113) * 2^exponent - // value = numerator * 2^(exponent - 113) - int final_exponent = exponent - QUAD_MANT_DIG; - - PyObject *denominator; - if (final_exponent >= 0) { - // Shift numerator left - PyObject *shift = PyLong_FromLong(final_exponent); - if (!shift) { - Py_DECREF(numerator); - return NULL; - } - PyObject *new_numerator = PyNumber_Lshift(numerator, shift); - Py_DECREF(shift); - Py_DECREF(numerator); - if (!new_numerator) { - return NULL; - } - numerator = new_numerator; - denominator = PyLong_FromLong(1); - } - else { - // Shift denominator left (denominator = 2^(-final_exponent)) - PyObject *shift = PyLong_FromLong(-final_exponent); - if (!shift) { - Py_DECREF(numerator); - return NULL; - } - PyObject *one = PyLong_FromLong(1); - if (!one) { - Py_DECREF(shift); - Py_DECREF(numerator); - return NULL; - } - denominator = PyNumber_Lshift(one, shift); - Py_DECREF(one); - Py_DECREF(shift); - if (!denominator) { - Py_DECREF(numerator); - return NULL; - } - } - - // Apply sign - if (is_negative) { - PyObject *new_numerator = PyNumber_Negative(numerator); - Py_DECREF(numerator); - if (!new_numerator) { - Py_DECREF(denominator); - return NULL; - } - numerator = new_numerator; - } - // Create and return the tuple - PyObject *result = PyTuple_Pack(2, numerator, denominator); - Py_DECREF(numerator); - Py_DECREF(denominator); - return result; } static PyMethodDef QuadPrecision_methods[] = { diff --git a/quaddtype/tests/test_quaddtype.py b/quaddtype/tests/test_quaddtype.py index ed92e2a..8098ddd 100644 --- a/quaddtype/tests/test_quaddtype.py +++ b/quaddtype/tests/test_quaddtype.py @@ -3510,237 +3510,147 @@ def test_fromfile_single_value(self): os.unlink(fname) -class TestFloatCompatibilityMethods: +class Test_Is_Integer_Methods: """Test suite for float compatibility methods: is_integer() and as_integer_ratio().""" - def test_is_integer_true_cases(self): - """Test is_integer() returns True for integer values.""" + @pytest.mark.parametrize("value,expected", [ # Positive integers - assert QuadPrecision("1.0").is_integer() - assert QuadPrecision("42.0").is_integer() - assert QuadPrecision("1000.0").is_integer() - + ("1.0", True), + ("42.0", True), + ("1000.0", True), # Negative integers - assert QuadPrecision("-1.0").is_integer() - assert QuadPrecision("-42.0").is_integer() - + ("-1.0", True), + ("-42.0", True), # Zero - assert QuadPrecision("0.0").is_integer() - assert QuadPrecision("-0.0").is_integer() - + ("0.0", True), + ("-0.0", True), # Large integers - assert QuadPrecision("1e20").is_integer() - assert QuadPrecision("123456789012345678901234567890").is_integer() - - def test_is_integer_false_cases(self): - """Test is_integer() returns False for non-integer values.""" - # Simple fractional values - assert not QuadPrecision("1.5").is_integer() - assert not QuadPrecision("3.14").is_integer() - assert not QuadPrecision("-2.5").is_integer() - - # Small fractional values - assert not QuadPrecision("0.1").is_integer() - assert not QuadPrecision("0.0001").is_integer() - + ("1e20", True), + ("123456789012345678901234567890", True), + # Fractional values + ("1.5", False), + ("3.14", False), + ("-2.5", False), + ("0.1", False), + ("0.0001", False), # Values close to integers but not exact - assert not QuadPrecision("1.0000000000001").is_integer() - assert not QuadPrecision("0.9999999999999").is_integer() - - def test_is_integer_special_values(self): - """Test is_integer() with special values.""" - # Infinity should return False - inf = QuadPrecision("inf") - assert not inf.is_integer() - - neg_inf = QuadPrecision("-inf") - assert not neg_inf.is_integer() - - # NaN should return False - nan = QuadPrecision("nan") - assert not nan.is_integer() + ("1.0000000000001", False), + ("0.9999999999999", False), + # Special values + ("inf", False), + ("-inf", False), + ("nan", False), + ]) + def test_is_integer(self, value, expected): + """Test is_integer() returns correct result for various values.""" + assert QuadPrecision(value).is_integer() == expected - def test_is_integer_compatibility_with_float(self): + @pytest.mark.parametrize("value", ["0.0", "1.0", "1.5", "-3.0", "-3.7", "42.0"]) + def test_is_integer_compatibility_with_float(self, value): """Test is_integer() matches behavior of Python's float.""" - test_values = ["0.0", "1.0", "1.5", "-3.0", "-3.7", "42.0"] - - for val_str in test_values: - quad_val = QuadPrecision(val_str) - float_val = float(val_str) - - assert quad_val.is_integer() == float_val.is_integer(), \ - f"Mismatch for {val_str}: quad={quad_val.is_integer()}, float={float_val.is_integer()}" - - def test_as_integer_ratio_simple_cases(self): - """Test as_integer_ratio() for simple values.""" - # Integer values - num, denom = QuadPrecision("1.0").as_integer_ratio() - assert num == 1 and denom == 1 - - num, denom = QuadPrecision("42.0").as_integer_ratio() - assert num == 42 and denom == 1 - - num, denom = QuadPrecision("-5.0").as_integer_ratio() - assert num == -5 and denom == 1 - - # Zero - num, denom = QuadPrecision("0.0").as_integer_ratio() - assert num == 0 and denom == 1 - - def test_as_integer_ratio_fractional_values(self): - """Test as_integer_ratio() for fractional values.""" - # 0.5 = 1/2 - num, denom = QuadPrecision("0.5").as_integer_ratio() - assert num / denom == 0.5 - assert denom > 0 # Denominator should always be positive - - # 0.25 = 1/4 - num, denom = QuadPrecision("0.25").as_integer_ratio() - assert num / denom == 0.25 - - # 1.5 = 3/2 - num, denom = QuadPrecision("1.5").as_integer_ratio() - assert num / denom == 1.5 - - # -2.5 = -5/2 - num, denom = QuadPrecision("-2.5").as_integer_ratio() - assert num / denom == -2.5 - - def test_as_integer_ratio_reconstruction(self): - """Test that as_integer_ratio() can reconstruct the original value.""" - test_values = ["3.14", "0.1", "1.414213562373095", "2.718281828459045", - "-1.23456789", "1000.001", "0.0001"] - - for val_str in test_values: - quad_val = QuadPrecision(val_str) - num, denom = quad_val.as_integer_ratio() - - # Reconstruct the value - reconstructed = QuadPrecision(num) / QuadPrecision(denom) - - # Should be very close (within quad precision limits) - assert reconstructed == quad_val, \ - f"Failed to reconstruct {val_str}: got {reconstructed} from {num}/{denom}" - - def test_as_integer_ratio_return_types(self): - """Test that as_integer_ratio() returns Python ints.""" - num, denom = QuadPrecision("3.14").as_integer_ratio() - - assert isinstance(num, int), f"Numerator should be int, got {type(num)}" - assert isinstance(denom, int), f"Denominator should be int, got {type(denom)}" - - def test_as_integer_ratio_denominator_positive(self): - """Test that denominator is always positive.""" - test_values = ["-1.0", "-3.14", "-0.5", "1.0", "3.14", "0.5"] - - for val_str in test_values: - num, denom = QuadPrecision(val_str).as_integer_ratio() - assert denom > 0, f"Denominator should be positive for {val_str}, got {denom}" - - def test_as_integer_ratio_infinity_raises(self): - """Test that as_integer_ratio() raises OverflowError for infinity.""" - inf = QuadPrecision("inf") - with pytest.raises(OverflowError, match="cannot convert Infinity to integer ratio"): - inf.as_integer_ratio() - - neg_inf = QuadPrecision("-inf") - with pytest.raises(OverflowError, match="cannot convert Infinity to integer ratio"): - neg_inf.as_integer_ratio() - - def test_as_integer_ratio_nan_raises(self): - """Test that as_integer_ratio() raises ValueError for NaN.""" - nan = QuadPrecision("nan") - with pytest.raises(ValueError, match="cannot convert NaN to integer ratio"): - nan.as_integer_ratio() - - def test_as_integer_ratio_compatibility_with_float(self): - """Test as_integer_ratio() matches behavior of Python's float where possible.""" - # For values that fit in float64 precision - test_values = ["1.0", "0.5", "3.14", "-2.5", "0.0"] - - for val_str in test_values: - quad_val = QuadPrecision(val_str) - float_val = float(val_str) - - quad_num, quad_denom = quad_val.as_integer_ratio() - float_num, float_denom = float_val.as_integer_ratio() - - # The ratios should be equal (though potentially in different reduced forms) - quad_ratio = quad_num / quad_denom - float_ratio = float_num / float_denom - - assert abs(quad_ratio - float_ratio) < 1e-15, \ - f"Mismatch for {val_str}: quad={quad_num}/{quad_denom}, float={float_num}/{float_denom}" - - def test_as_integer_ratio_large_values(self): - """Test as_integer_ratio() with large values.""" - # Large integer - large_val = QuadPrecision("1e20") - num, denom = large_val.as_integer_ratio() - assert num / denom == float(str(large_val)) - - # Large fractional value - large_frac = QuadPrecision("1.23e15") - num, denom = large_frac.as_integer_ratio() - reconstructed = num / denom - assert abs(reconstructed - 1.23e15) < 1e10 # Allow some tolerance for large numbers - - def test_as_integer_ratio_small_values(self): - """Test as_integer_ratio() with very small values.""" - small_val = QuadPrecision("1e-30") - num, denom = small_val.as_integer_ratio() - - # Reconstruct and verify - reconstructed = QuadPrecision(num) / QuadPrecision(denom) - assert reconstructed == small_val - - def test_methods_available_on_type(self): - """Test that methods are available on the QuadPrecision class.""" - # Check that the methods exist - assert hasattr(QuadPrecision, 'is_integer') - assert hasattr(QuadPrecision, 'as_integer_ratio') - - # Check that they're callable - assert callable(getattr(QuadPrecision, 'is_integer')) - assert callable(getattr(QuadPrecision, 'as_integer_ratio')) - - def test_methods_available_on_instance(self): - """Test that methods are available on QuadPrecision instances.""" - val = QuadPrecision("3.14") - - # Check that the methods exist on the instance - assert hasattr(val, 'is_integer') - assert hasattr(val, 'as_integer_ratio') - - # Check that they're callable - assert callable(val.is_integer) - assert callable(val.as_integer_ratio) - - def test_is_integer_with_different_backends(self): - """Test is_integer() with both SLEEF and longdouble backends.""" - # Note: This test assumes longdouble backend is available - try: - sleef_val = QuadPrecision("3.0", backend="sleef") - assert sleef_val.is_integer() - - # Only test longdouble if it's actually 128-bit - if numpy_quaddtype.is_longdouble_128(): - ld_val = QuadPrecision("3.0", backend="longdouble") - assert ld_val.is_integer() - except Exception: - pytest.skip("Backend not available") - - def test_as_integer_ratio_with_different_backends(self): - """Test as_integer_ratio() with both SLEEF and longdouble backends.""" - try: - sleef_val = QuadPrecision("1.5", backend="sleef") - sleef_num, sleef_denom = sleef_val.as_integer_ratio() - assert sleef_num / sleef_denom == 1.5 - - # Only test longdouble if it's actually 128-bit - if numpy_quaddtype.is_longdouble_128(): - ld_val = QuadPrecision("1.5", backend="longdouble") - ld_num, ld_denom = ld_val.as_integer_ratio() - assert ld_num / ld_denom == 1.5 - except Exception: - pytest.skip("Backend not available") + quad_val = QuadPrecision(value) + float_val = float(value) + assert quad_val.is_integer() == float_val.is_integer() + + # @pytest.mark.parametrize("value,expected_num,expected_denom", [ + # ("1.0", 1, 1), + # ("42.0", 42, 1), + # ("-5.0", -5, 1), + # ("0.0", 0, 1), + # ]) + # def test_as_integer_ratio_integers(self, value, expected_num, expected_denom): + # """Test as_integer_ratio() for integer values.""" + # num, denom = QuadPrecision(value).as_integer_ratio() + # assert num == expected_num and denom == expected_denom + + # @pytest.mark.parametrize("value,expected_ratio", [ + # ("0.5", 0.5), + # ("0.25", 0.25), + # ("1.5", 1.5), + # ("-2.5", -2.5), + # ]) + # def test_as_integer_ratio_fractional(self, value, expected_ratio): + # """Test as_integer_ratio() for fractional values.""" + # num, denom = QuadPrecision(value).as_integer_ratio() + # assert num / denom == expected_ratio + # assert denom > 0 # Denominator should always be positive + + # @pytest.mark.parametrize("value", [ + # "3.14", "0.1", "1.414213562373095", "2.718281828459045", + # "-1.23456789", "1000.001", "0.0001", "1e20", "1.23e15", "1e-30" + # ]) + # def test_as_integer_ratio_reconstruction(self, value): + # """Test that as_integer_ratio() can reconstruct the original value.""" + # quad_val = QuadPrecision(value) + # num, denom = quad_val.as_integer_ratio() + # reconstructed = QuadPrecision(num) / QuadPrecision(denom) + # assert reconstructed == quad_val + + # def test_as_integer_ratio_return_types(self): + # """Test that as_integer_ratio() returns Python ints.""" + # num, denom = QuadPrecision("3.14").as_integer_ratio() + # assert isinstance(num, int) + # assert isinstance(denom, int) + + # @pytest.mark.parametrize("value", ["-1.0", "-3.14", "-0.5", "1.0", "3.14", "0.5"]) + # def test_as_integer_ratio_denominator_positive(self, value): + # """Test that denominator is always positive.""" + # num, denom = QuadPrecision(value).as_integer_ratio() + # assert denom > 0 + + # @pytest.mark.parametrize("value,exception,match", [ + # ("inf", OverflowError, "cannot convert Infinity to integer ratio"), + # ("-inf", OverflowError, "cannot convert Infinity to integer ratio"), + # ("nan", ValueError, "cannot convert NaN to integer ratio"), + # ]) + # def test_as_integer_ratio_special_values_raise(self, value, exception, match): + # """Test that as_integer_ratio() raises appropriate errors for special values.""" + # with pytest.raises(exception, match=match): + # QuadPrecision(value).as_integer_ratio() + + # @pytest.mark.parametrize("value", ["1.0", "0.5", "3.14", "-2.5", "0.0"]) + # def test_as_integer_ratio_compatibility_with_float(self, value): + # """Test as_integer_ratio() matches behavior of Python's float where possible.""" + # quad_val = QuadPrecision(value) + # float_val = float(value) + + # quad_num, quad_denom = quad_val.as_integer_ratio() + # float_num, float_denom = float_val.as_integer_ratio() + + # # The ratios should be equal + # quad_ratio = quad_num / quad_denom + # float_ratio = float_num / float_denom + # assert abs(quad_ratio - float_ratio) < 1e-15 + + # def test_methods_available_on_type_and_instance(self): + # """Test that methods are available on the QuadPrecision class and instances.""" + # # Check on type + # assert hasattr(QuadPrecision, 'is_integer') and callable(QuadPrecision.is_integer) + # assert hasattr(QuadPrecision, 'as_integer_ratio') and callable(QuadPrecision.as_integer_ratio) + + # # Check on instance + # val = QuadPrecision("3.14") + # assert hasattr(val, 'is_integer') and callable(val.is_integer) + # assert hasattr(val, 'as_integer_ratio') and callable(val.as_integer_ratio) + + # @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + # def test_is_integer_with_backends(self, backend): + # """Test is_integer() with different backends.""" + # if backend == "longdouble" and not numpy_quaddtype.is_longdouble_128(): + # pytest.skip("longdouble backend not 128-bit") + + # val = QuadPrecision("3.0", backend=backend) + # assert val.is_integer() + + # val2 = QuadPrecision("3.5", backend=backend) + # assert not val2.is_integer() + + # @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + # def test_as_integer_ratio_with_backends(self, backend): + # """Test as_integer_ratio() with different backends.""" + # if backend == "longdouble" and not numpy_quaddtype.is_longdouble_128(): + # pytest.skip("longdouble backend not 128-bit") + + # val = QuadPrecision("1.5", backend=backend) + # num, denom = val.as_integer_ratio() + # assert num / denom == 1.5 From b8a60a6b4ff7d669f693b95a4bef2aedd0ed2077 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 31 Oct 2025 14:43:12 +0000 Subject: [PATCH 3/9] need to fix thread-safety --- quaddtype/numpy_quaddtype/src/scalar.c | 127 ++++++++++++++++++ quaddtype/tests/test_quaddtype.py | 173 ++++++++++--------------- 2 files changed, 198 insertions(+), 102 deletions(-) diff --git a/quaddtype/numpy_quaddtype/src/scalar.c b/quaddtype/numpy_quaddtype/src/scalar.c index e959d4b..98936fc 100644 --- a/quaddtype/numpy_quaddtype/src/scalar.c +++ b/quaddtype/numpy_quaddtype/src/scalar.c @@ -422,10 +422,137 @@ QuadPrecision_is_integer(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored) } } +// this is thread-unsafe +PyObject* quad_to_pylong(Sleef_quad value) +{ + char buffer[128]; + // Format as integer (%.0Qf gives integer with no decimal places) + // Q modifier means pass Sleef_quad by value + int written = Sleef_snprintf(buffer, sizeof(buffer), "%.0Qf", value); + if (written < 0 || written >= sizeof(buffer)) { + PyErr_SetString(PyExc_RuntimeError, "Failed to convert quad to string"); + return NULL; + } + + PyObject *result = PyLong_FromString(buffer, NULL, 10); + + if (result == NULL) { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse integer string"); + return NULL; + } + + return result; +} + +// inspired by the CPython implementation +// https://github.com/python/cpython/blob/ac1ffd77858b62d169a08040c08aa5de26e145ac/Objects/floatobject.c#L1503C1-L1572C2 +// NOTE: a 128-bit static PyObject * QuadPrecision_as_integer_ratio(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored)) { + + Sleef_quad value; + Sleef_quad pos_inf = sleef_q(+0x1000000000000LL, 0x0000000000000000ULL, 16384); + const int FLOAT128_PRECISION = 113; + if (self->backend == BACKEND_SLEEF) { + value = self->value.sleef_value; + } + else { + // lets also tackle ld from sleef functions as well + value = Sleef_cast_from_doubleq1((double)self->value.longdouble_value); + } + + if(Sleef_iunordq1(value, value)) { + PyErr_SetString(PyExc_ValueError, "Cannot convert NaN to integer ratio"); + return NULL; + } + if(Sleef_icmpgeq1(Sleef_fabsq1(value), pos_inf)) { + PyErr_SetString(PyExc_OverflowError, "Cannot convert infinite value to integer ratio"); + return NULL; + } + + // Sleef_value == float_part * 2**exponent exactly + int exponent; + Sleef_quad mantissa = Sleef_frexpq1(value, &exponent); // within [0.5, 1.0) + + /* + CPython loops for 300 (some huge number) to make sure + float_part gets converted to the floor(float_part) i.e. near integer as + + for (i=0; i<300 && float_part != floor(float_part) ; i++) { + float_part *= 2.0; + exponent--; + } + + It seems highly inefficient from performance perspective, maybe they pick 300 for future-proof + or If FLT_RADIX != 2, the 300 steps may leave a tiny fractional part + + Another way can be doing as: + ``` + mantissa = ldexpq(mantissa, FLOAT128_PRECISION); + exponent -= FLOAT128_PRECISION; + ``` + This should work but give non-simplified, huge integers (although they also come down to same representation) + We can also do gcd to find simplified values, but it'll add more O(log(N)) {which in theory seem better} + For the sake of simplicity and fixed 128-bit nature, we will loop till 113 only + */ + + for (int i = 0; i < FLOAT128_PRECISION && !Sleef_icmpeqq1(mantissa, Sleef_floorq1(mantissa)); i++) { + mantissa = Sleef_mulq1_u05(mantissa, Sleef_cast_from_doubleq1(2.0)); + exponent--; + } + + + // numerator and denominators can't fit in int + // convert items to PyLongObject from string instead + + PyObject *py_exp = PyLong_FromLongLong(Py_ABS(exponent)); + if(py_exp == NULL) + { + return NULL; + } + + PyObject *numerator = quad_to_pylong(mantissa); + if(numerator == NULL) + { + Py_DECREF(numerator); + return NULL; + } + PyObject *denominator = PyLong_FromLong(1); + if (denominator == NULL) { + Py_DECREF(numerator); + return NULL; + } + + // fold in 2**exponent + if(exponent > 0) + { + PyObject *new_num = PyNumber_Lshift(numerator, py_exp); + Py_DECREF(numerator); + if(new_num == NULL) + { + Py_DECREF(denominator); + Py_DECREF(py_exp); + return NULL; + } + numerator = new_num; + } + else + { + PyObject *new_denom = PyNumber_Lshift(denominator, py_exp); + Py_DECREF(denominator); + if(new_denom == NULL) + { + Py_DECREF(numerator); + Py_DECREF(py_exp); + return NULL; + } + denominator = new_denom; + } + + Py_DECREF(py_exp); + return PyTuple_Pack(2, numerator, denominator); } static PyMethodDef QuadPrecision_methods[] = { diff --git a/quaddtype/tests/test_quaddtype.py b/quaddtype/tests/test_quaddtype.py index 8098ddd..ae8d5b5 100644 --- a/quaddtype/tests/test_quaddtype.py +++ b/quaddtype/tests/test_quaddtype.py @@ -3552,105 +3552,74 @@ def test_is_integer_compatibility_with_float(self, value): float_val = float(value) assert quad_val.is_integer() == float_val.is_integer() - # @pytest.mark.parametrize("value,expected_num,expected_denom", [ - # ("1.0", 1, 1), - # ("42.0", 42, 1), - # ("-5.0", -5, 1), - # ("0.0", 0, 1), - # ]) - # def test_as_integer_ratio_integers(self, value, expected_num, expected_denom): - # """Test as_integer_ratio() for integer values.""" - # num, denom = QuadPrecision(value).as_integer_ratio() - # assert num == expected_num and denom == expected_denom - - # @pytest.mark.parametrize("value,expected_ratio", [ - # ("0.5", 0.5), - # ("0.25", 0.25), - # ("1.5", 1.5), - # ("-2.5", -2.5), - # ]) - # def test_as_integer_ratio_fractional(self, value, expected_ratio): - # """Test as_integer_ratio() for fractional values.""" - # num, denom = QuadPrecision(value).as_integer_ratio() - # assert num / denom == expected_ratio - # assert denom > 0 # Denominator should always be positive - - # @pytest.mark.parametrize("value", [ - # "3.14", "0.1", "1.414213562373095", "2.718281828459045", - # "-1.23456789", "1000.001", "0.0001", "1e20", "1.23e15", "1e-30" - # ]) - # def test_as_integer_ratio_reconstruction(self, value): - # """Test that as_integer_ratio() can reconstruct the original value.""" - # quad_val = QuadPrecision(value) - # num, denom = quad_val.as_integer_ratio() - # reconstructed = QuadPrecision(num) / QuadPrecision(denom) - # assert reconstructed == quad_val - - # def test_as_integer_ratio_return_types(self): - # """Test that as_integer_ratio() returns Python ints.""" - # num, denom = QuadPrecision("3.14").as_integer_ratio() - # assert isinstance(num, int) - # assert isinstance(denom, int) - - # @pytest.mark.parametrize("value", ["-1.0", "-3.14", "-0.5", "1.0", "3.14", "0.5"]) - # def test_as_integer_ratio_denominator_positive(self, value): - # """Test that denominator is always positive.""" - # num, denom = QuadPrecision(value).as_integer_ratio() - # assert denom > 0 - - # @pytest.mark.parametrize("value,exception,match", [ - # ("inf", OverflowError, "cannot convert Infinity to integer ratio"), - # ("-inf", OverflowError, "cannot convert Infinity to integer ratio"), - # ("nan", ValueError, "cannot convert NaN to integer ratio"), - # ]) - # def test_as_integer_ratio_special_values_raise(self, value, exception, match): - # """Test that as_integer_ratio() raises appropriate errors for special values.""" - # with pytest.raises(exception, match=match): - # QuadPrecision(value).as_integer_ratio() - - # @pytest.mark.parametrize("value", ["1.0", "0.5", "3.14", "-2.5", "0.0"]) - # def test_as_integer_ratio_compatibility_with_float(self, value): - # """Test as_integer_ratio() matches behavior of Python's float where possible.""" - # quad_val = QuadPrecision(value) - # float_val = float(value) - - # quad_num, quad_denom = quad_val.as_integer_ratio() - # float_num, float_denom = float_val.as_integer_ratio() - - # # The ratios should be equal - # quad_ratio = quad_num / quad_denom - # float_ratio = float_num / float_denom - # assert abs(quad_ratio - float_ratio) < 1e-15 - - # def test_methods_available_on_type_and_instance(self): - # """Test that methods are available on the QuadPrecision class and instances.""" - # # Check on type - # assert hasattr(QuadPrecision, 'is_integer') and callable(QuadPrecision.is_integer) - # assert hasattr(QuadPrecision, 'as_integer_ratio') and callable(QuadPrecision.as_integer_ratio) - - # # Check on instance - # val = QuadPrecision("3.14") - # assert hasattr(val, 'is_integer') and callable(val.is_integer) - # assert hasattr(val, 'as_integer_ratio') and callable(val.as_integer_ratio) - - # @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) - # def test_is_integer_with_backends(self, backend): - # """Test is_integer() with different backends.""" - # if backend == "longdouble" and not numpy_quaddtype.is_longdouble_128(): - # pytest.skip("longdouble backend not 128-bit") - - # val = QuadPrecision("3.0", backend=backend) - # assert val.is_integer() - - # val2 = QuadPrecision("3.5", backend=backend) - # assert not val2.is_integer() - - # @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) - # def test_as_integer_ratio_with_backends(self, backend): - # """Test as_integer_ratio() with different backends.""" - # if backend == "longdouble" and not numpy_quaddtype.is_longdouble_128(): - # pytest.skip("longdouble backend not 128-bit") - - # val = QuadPrecision("1.5", backend=backend) - # num, denom = val.as_integer_ratio() - # assert num / denom == 1.5 + @pytest.mark.parametrize("value,expected_num,expected_denom", [ + ("1.0", 1, 1), + ("42.0", 42, 1), + ("-5.0", -5, 1), + ("0.0", 0, 1), + ("-0.0", 0, 1), + ]) + def test_as_integer_ratio_integers(self, value, expected_num, expected_denom): + """Test as_integer_ratio() for integer values.""" + num, denom = QuadPrecision(value).as_integer_ratio() + assert num == expected_num and denom == expected_denom + + @pytest.mark.parametrize("value,expected_ratio", [ + ("0.5", 0.5), + ("0.25", 0.25), + ("1.5", 1.5), + ("-2.5", -2.5), + ]) + def test_as_integer_ratio_fractional(self, value, expected_ratio): + """Test as_integer_ratio() for fractional values.""" + num, denom = QuadPrecision(value).as_integer_ratio() + assert QuadPrecision(str(num)) / QuadPrecision(str(denom)) == QuadPrecision(str(expected_ratio)) + assert denom > 0 # Denominator should always be positive + + @pytest.mark.parametrize("value", [ + "3.14", "0.1", "1.414213562373095", "2.718281828459045", + "-1.23456789", "1000.001", "0.0001", "1e20", "1.23e15", "1e-30", quad_pi + ]) + def test_as_integer_ratio_reconstruction(self, value): + """Test that as_integer_ratio() can reconstruct the original value.""" + quad_val = QuadPrecision(value) + num, denom = quad_val.as_integer_ratio() + # todo: can remove str converstion after merging PR #213 + reconstructed = QuadPrecision(str(num)) / QuadPrecision(str(denom)) + assert reconstructed == quad_val + + def test_as_integer_ratio_return_types(self): + """Test that as_integer_ratio() returns Python ints.""" + num, denom = QuadPrecision("3.14").as_integer_ratio() + assert isinstance(num, int) + assert isinstance(denom, int) + + @pytest.mark.parametrize("value", ["-1.0", "-3.14", "-0.5", "1.0", "3.14", "0.5"]) + def test_as_integer_ratio_denominator_positive(self, value): + """Test that denominator is always positive.""" + num, denom = QuadPrecision(value).as_integer_ratio() + assert denom > 0 + + @pytest.mark.parametrize("value,exception,match", [ + ("inf", OverflowError, "Cannot convert infinite value to integer ratio"), + ("-inf", OverflowError, "Cannot convert infinite value to integer ratio"), + ("nan", ValueError, "Cannot convert NaN to integer ratio"), + ]) + def test_as_integer_ratio_special_values_raise(self, value, exception, match): + """Test that as_integer_ratio() raises appropriate errors for special values.""" + with pytest.raises(exception, match=match): + QuadPrecision(value).as_integer_ratio() + + @pytest.mark.parametrize("value", ["1.0", "0.5", "3.14", "-2.5", "0.0"]) + def test_as_integer_ratio_compatibility_with_float(self, value): + """Test as_integer_ratio() matches behavior of Python's float where possible.""" + quad_val = QuadPrecision(value) + float_val = float(value) + + quad_num, quad_denom = quad_val.as_integer_ratio() + float_num, float_denom = float_val.as_integer_ratio() + + # The ratios should be equal + quad_ratio = quad_num / quad_denom + float_ratio = float_num / float_denom + assert abs(quad_ratio - float_ratio) < 1e-15 \ No newline at end of file From 9f95d210fc781b99ffc9dc6934aa9632867dff83 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 31 Oct 2025 16:29:54 +0000 Subject: [PATCH 4/9] adding multithreading test --- quaddtype/numpy_quaddtype/src/scalar.c | 2 +- quaddtype/tests/test_multithreading.py | 36 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 quaddtype/tests/test_multithreading.py diff --git a/quaddtype/numpy_quaddtype/src/scalar.c b/quaddtype/numpy_quaddtype/src/scalar.c index 98936fc..3e766ff 100644 --- a/quaddtype/numpy_quaddtype/src/scalar.c +++ b/quaddtype/numpy_quaddtype/src/scalar.c @@ -494,7 +494,7 @@ QuadPrecision_as_integer_ratio(QuadPrecisionObject *self, PyObject *Py_UNUSED(ig exponent -= FLOAT128_PRECISION; ``` This should work but give non-simplified, huge integers (although they also come down to same representation) - We can also do gcd to find simplified values, but it'll add more O(log(N)) {which in theory seem better} + We can also do gcd to find simplified values, but it'll add more O(log(N)) For the sake of simplicity and fixed 128-bit nature, we will loop till 113 only */ diff --git a/quaddtype/tests/test_multithreading.py b/quaddtype/tests/test_multithreading.py new file mode 100644 index 0000000..f65875f --- /dev/null +++ b/quaddtype/tests/test_multithreading.py @@ -0,0 +1,36 @@ +import concurrent.futures +import threading + +import pytest + +import numpy as np +from numpy._core import _rational_tests +from numpy._core.tests.test_stringdtype import random_unicode_string_list +from numpy.testing import IS_64BIT, IS_WASM +from numpy.testing._private.utils import run_threaded + +if IS_WASM: + pytest.skip(allow_module_level=True, reason="no threading support in wasm") + +pytestmark = pytest.mark.thread_unsafe( + reason="tests in this module are already explicitly multi-threaded" +) + +from numpy_quaddtype import * + + +def test_as_integer_ratio_reconstruction(): + """Multi-threaded test that as_integer_ratio() can reconstruct the original value.""" + values = ["3.14", "0.1", "1.414213562373095", "2.718281828459045", + "-1.23456789", "1000.001", "0.0001", "1e20", "1.23e15", "1e-30", pi] + + def test(barrier): + barrier.wait() # All threads start simultaneously + for val in values: + quad_val = QuadPrecision(val) + num, denom = quad_val.as_integer_ratio() + # todo: can remove str converstion after merging PR #213 + reconstructed = QuadPrecision(str(num)) / QuadPrecision(str(denom)) + assert reconstructed == quad_val + + run_threaded(test, pass_barrier=True, max_workers=64, outer_iterations=100) \ No newline at end of file From 72b2023303f7496084230831d56cf3e1e7fb43a8 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 31 Oct 2025 17:53:50 +0000 Subject: [PATCH 5/9] lock init on py < 3.13 --- quaddtype/numpy_quaddtype/src/scalar.c | 25 +++++++++++++++++++++++-- quaddtype/tests/test_multithreading.py | 4 ++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/quaddtype/numpy_quaddtype/src/scalar.c b/quaddtype/numpy_quaddtype/src/scalar.c index 3e766ff..6a7d24c 100644 --- a/quaddtype/numpy_quaddtype/src/scalar.c +++ b/quaddtype/numpy_quaddtype/src/scalar.c @@ -22,6 +22,18 @@ // src: https://en.wikipedia.org/wiki/Quadruple-precision_floating-point_format #define SLEEF_QUAD_DECIMAL_DIG 36 +#if PY_VERSION_HEX < 0x30d00b3 +static PyThread_type_lock sleef_lock; +#define LOCK_SLEEF PyThread_acquire_lock(sleef_lock, WAIT_LOCK) +#define UNLOCK_SLEEF PyThread_release_lock(sleef_lock) +#else +static PyMutex sleef_lock = {0}; +#define LOCK_SLEEF PyMutex_Lock(&sleef_lock) +#define UNLOCK_SLEEF PyMutex_Unlock(&sleef_lock) +#endif + + + QuadPrecisionObject * QuadPrecision_raw_new(QuadBackendType backend) @@ -422,13 +434,16 @@ QuadPrecision_is_integer(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored) } } -// this is thread-unsafe PyObject* quad_to_pylong(Sleef_quad value) { char buffer[128]; + + // Sleef_snprintf call is thread-unsafe + // LOCK_SLEEF; // Format as integer (%.0Qf gives integer with no decimal places) // Q modifier means pass Sleef_quad by value int written = Sleef_snprintf(buffer, sizeof(buffer), "%.0Qf", value); + // UNLOCK_SLEEF; if (written < 0 || written >= sizeof(buffer)) { PyErr_SetString(PyExc_RuntimeError, "Failed to convert quad to string"); return NULL; @@ -503,7 +518,6 @@ QuadPrecision_as_integer_ratio(QuadPrecisionObject *self, PyObject *Py_UNUSED(ig exponent--; } - // numerator and denominators can't fit in int // convert items to PyLongObject from string instead @@ -587,6 +601,13 @@ PyTypeObject QuadPrecision_Type = { int init_quadprecision_scalar(void) { +#if PY_VERSION_HEX < 0x30d00b3 + sleef_lock = PyThread_allocate_lock(); + if (sleef_lock == NULL) { + PyErr_NoMemory(); + return -1; + } +#endif QuadPrecision_Type.tp_base = &PyFloatingArrType_Type; return PyType_Ready(&QuadPrecision_Type); } \ No newline at end of file diff --git a/quaddtype/tests/test_multithreading.py b/quaddtype/tests/test_multithreading.py index f65875f..34d89f8 100644 --- a/quaddtype/tests/test_multithreading.py +++ b/quaddtype/tests/test_multithreading.py @@ -21,11 +21,11 @@ def test_as_integer_ratio_reconstruction(): """Multi-threaded test that as_integer_ratio() can reconstruct the original value.""" - values = ["3.14", "0.1", "1.414213562373095", "2.718281828459045", - "-1.23456789", "1000.001", "0.0001", "1e20", "1.23e15", "1e-30", pi] def test(barrier): barrier.wait() # All threads start simultaneously + values = ["3.14", "0.1", "1.414213562373095", "2.718281828459045", + "-1.23456789", "1000.001", "0.0001", "1e20", "1.23e15", "1e-30", pi] for val in values: quad_val = QuadPrecision(val) num, denom = quad_val.as_integer_ratio() From 652c35fdb55fb20d83c106ca26afba29e519df3e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 31 Oct 2025 17:54:40 +0000 Subject: [PATCH 6/9] only lock call --- quaddtype/numpy_quaddtype/src/scalar.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quaddtype/numpy_quaddtype/src/scalar.c b/quaddtype/numpy_quaddtype/src/scalar.c index 6a7d24c..57706fe 100644 --- a/quaddtype/numpy_quaddtype/src/scalar.c +++ b/quaddtype/numpy_quaddtype/src/scalar.c @@ -439,11 +439,11 @@ PyObject* quad_to_pylong(Sleef_quad value) char buffer[128]; // Sleef_snprintf call is thread-unsafe - // LOCK_SLEEF; + LOCK_SLEEF; // Format as integer (%.0Qf gives integer with no decimal places) // Q modifier means pass Sleef_quad by value int written = Sleef_snprintf(buffer, sizeof(buffer), "%.0Qf", value); - // UNLOCK_SLEEF; + UNLOCK_SLEEF; if (written < 0 || written >= sizeof(buffer)) { PyErr_SetString(PyExc_RuntimeError, "Failed to convert quad to string"); return NULL; From 9c54981110b0d77938aba29d31d87566a70e5b6d Mon Sep 17 00:00:00 2001 From: swayaminsync Date: Sat, 1 Nov 2025 05:37:05 +0530 Subject: [PATCH 7/9] debugging and TSan build info --- quaddtype/reinstall.sh | 8 ++++++-- quaddtype/subprojects/packagefiles/sleef/meson.build | 5 +++++ quaddtype/tests/test_multithreading.py | 4 ---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/quaddtype/reinstall.sh b/quaddtype/reinstall.sh index 9144f05..cd2a8ed 100755 --- a/quaddtype/reinstall.sh +++ b/quaddtype/reinstall.sh @@ -8,7 +8,11 @@ if [ -d "build/" ]; then rm -rf subprojects/sleef fi -# export CFLAGS="-g -O0" -# export CXXFLAGS="-g -O0" python -m pip uninstall -y numpy_quaddtype python -m pip install . -vv --no-build-isolation 2>&1 | tee build_log.txt + +# for debugging and TSAN builds, comment the above line and uncomment all below: +# export CFLAGS="-fsanitize=thread -g -O0" +# export CXXFLAGS="-fsanitize=thread -g -O0" +# export LDFLAGS="-fsanitize=thread" +# python -m pip install . -vv --no-build-isolation -Csetup-args=-Db_sanitize=thread 2>&1 | tee build_log.txt \ No newline at end of file diff --git a/quaddtype/subprojects/packagefiles/sleef/meson.build b/quaddtype/subprojects/packagefiles/sleef/meson.build index 20faeff..141e50b 100644 --- a/quaddtype/subprojects/packagefiles/sleef/meson.build +++ b/quaddtype/subprojects/packagefiles/sleef/meson.build @@ -12,6 +12,7 @@ if host_machine.system() == 'windows' parallel_flag = [] endif +# uncomment below lines for TSAN builds (in case compiler flags are not picked up from meson) sleef_configure = run_command([ cmake, '-S', meson.current_source_dir(), @@ -22,6 +23,10 @@ sleef_configure = run_command([ '-DSLEEF_BUILD_TESTS=OFF', '-DSLEEF_BUILD_INLINE_HEADERS=OFF', '-DCMAKE_POSITION_INDEPENDENT_CODE=ON', + # '-DCMAKE_C_FLAGS=-fsanitize=thread -g', + # '-DCMAKE_CXX_FLAGS=-fsanitize=thread -g', + # '-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=thread', + # '-DCMAKE_SHARED_LINKER_FLAGS=-fsanitize=thread', '-DCMAKE_INSTALL_PREFIX=' + meson.current_build_dir() / sleef_install_dir ], check: false, capture: true) diff --git a/quaddtype/tests/test_multithreading.py b/quaddtype/tests/test_multithreading.py index 34d89f8..90eb257 100644 --- a/quaddtype/tests/test_multithreading.py +++ b/quaddtype/tests/test_multithreading.py @@ -12,10 +12,6 @@ if IS_WASM: pytest.skip(allow_module_level=True, reason="no threading support in wasm") -pytestmark = pytest.mark.thread_unsafe( - reason="tests in this module are already explicitly multi-threaded" -) - from numpy_quaddtype import * From 9ffcbfd74f163c5509522bd9cd200ebf344f89e4 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 1 Nov 2025 12:55:37 +0000 Subject: [PATCH 8/9] adding stubs --- quaddtype/numpy_quaddtype/_quaddtype_main.pyi | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/quaddtype/numpy_quaddtype/_quaddtype_main.pyi b/quaddtype/numpy_quaddtype/_quaddtype_main.pyi index 566ff99..1382d94 100644 --- a/quaddtype/numpy_quaddtype/_quaddtype_main.pyi +++ b/quaddtype/numpy_quaddtype/_quaddtype_main.pyi @@ -1,5 +1,5 @@ from typing import Any, Literal, TypeAlias, final, overload - +import builtins import numpy as np from numpy._typing import _128Bit # pyright: ignore[reportPrivateUsage] from typing_extensions import Never, Self, override @@ -157,9 +157,8 @@ class QuadPrecision(np.floating[_128Bit]): # NOTE: is_integer() and as_integer_ratio() are defined on numpy.floating in the # stubs, but don't exist at runtime. And because QuadPrecision does not implement # them, we use this hacky workaround to emulate their absence. - # TODO: Remove after https://github.com/numpy/numpy-user-dtypes/issues/216 - is_integer: Never # pyright: ignore[reportIncompatibleMethodOverride] - as_integer_ratio: Never # pyright: ignore[reportIncompatibleMethodOverride] + def is_integer(self, /) -> builtins.bool: ... + def as_integer_ratio(self, /) -> tuple[int, int]: ... # def is_longdouble_128() -> bool: ... From 476ac4dd112bfed3dd7f79640264dff7086ffe49 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 1 Nov 2025 14:37:12 +0000 Subject: [PATCH 9/9] decorating stub --- quaddtype/numpy_quaddtype/_quaddtype_main.pyi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quaddtype/numpy_quaddtype/_quaddtype_main.pyi b/quaddtype/numpy_quaddtype/_quaddtype_main.pyi index 1382d94..831c073 100644 --- a/quaddtype/numpy_quaddtype/_quaddtype_main.pyi +++ b/quaddtype/numpy_quaddtype/_quaddtype_main.pyi @@ -157,7 +157,9 @@ class QuadPrecision(np.floating[_128Bit]): # NOTE: is_integer() and as_integer_ratio() are defined on numpy.floating in the # stubs, but don't exist at runtime. And because QuadPrecision does not implement # them, we use this hacky workaround to emulate their absence. + @override def is_integer(self, /) -> builtins.bool: ... + @override def as_integer_ratio(self, /) -> tuple[int, int]: ... #