Skip to content

Commit b8a60a6

Browse files
committed
need to fix thread-safety
1 parent cb3243d commit b8a60a6

File tree

2 files changed

+198
-102
lines changed

2 files changed

+198
-102
lines changed

quaddtype/numpy_quaddtype/src/scalar.c

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,10 +422,137 @@ QuadPrecision_is_integer(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored)
422422
}
423423
}
424424

425+
// this is thread-unsafe
426+
PyObject* quad_to_pylong(Sleef_quad value)
427+
{
428+
char buffer[128];
429+
// Format as integer (%.0Qf gives integer with no decimal places)
430+
// Q modifier means pass Sleef_quad by value
431+
int written = Sleef_snprintf(buffer, sizeof(buffer), "%.0Qf", value);
432+
if (written < 0 || written >= sizeof(buffer)) {
433+
PyErr_SetString(PyExc_RuntimeError, "Failed to convert quad to string");
434+
return NULL;
435+
}
436+
437+
PyObject *result = PyLong_FromString(buffer, NULL, 10);
438+
439+
if (result == NULL) {
440+
PyErr_SetString(PyExc_RuntimeError, "Failed to parse integer string");
441+
return NULL;
442+
}
443+
444+
return result;
445+
}
446+
447+
// inspired by the CPython implementation
448+
// https://github.com/python/cpython/blob/ac1ffd77858b62d169a08040c08aa5de26e145ac/Objects/floatobject.c#L1503C1-L1572C2
449+
// NOTE: a 128-bit
425450
static PyObject *
426451
QuadPrecision_as_integer_ratio(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored))
427452
{
453+
454+
Sleef_quad value;
455+
Sleef_quad pos_inf = sleef_q(+0x1000000000000LL, 0x0000000000000000ULL, 16384);
456+
const int FLOAT128_PRECISION = 113;
428457

458+
if (self->backend == BACKEND_SLEEF) {
459+
value = self->value.sleef_value;
460+
}
461+
else {
462+
// lets also tackle ld from sleef functions as well
463+
value = Sleef_cast_from_doubleq1((double)self->value.longdouble_value);
464+
}
465+
466+
if(Sleef_iunordq1(value, value)) {
467+
PyErr_SetString(PyExc_ValueError, "Cannot convert NaN to integer ratio");
468+
return NULL;
469+
}
470+
if(Sleef_icmpgeq1(Sleef_fabsq1(value), pos_inf)) {
471+
PyErr_SetString(PyExc_OverflowError, "Cannot convert infinite value to integer ratio");
472+
return NULL;
473+
}
474+
475+
// Sleef_value == float_part * 2**exponent exactly
476+
int exponent;
477+
Sleef_quad mantissa = Sleef_frexpq1(value, &exponent); // within [0.5, 1.0)
478+
479+
/*
480+
CPython loops for 300 (some huge number) to make sure
481+
float_part gets converted to the floor(float_part) i.e. near integer as
482+
483+
for (i=0; i<300 && float_part != floor(float_part) ; i++) {
484+
float_part *= 2.0;
485+
exponent--;
486+
}
487+
488+
It seems highly inefficient from performance perspective, maybe they pick 300 for future-proof
489+
or If FLT_RADIX != 2, the 300 steps may leave a tiny fractional part
490+
491+
Another way can be doing as:
492+
```
493+
mantissa = ldexpq(mantissa, FLOAT128_PRECISION);
494+
exponent -= FLOAT128_PRECISION;
495+
```
496+
This should work but give non-simplified, huge integers (although they also come down to same representation)
497+
We can also do gcd to find simplified values, but it'll add more O(log(N)) {which in theory seem better}
498+
For the sake of simplicity and fixed 128-bit nature, we will loop till 113 only
499+
*/
500+
501+
for (int i = 0; i < FLOAT128_PRECISION && !Sleef_icmpeqq1(mantissa, Sleef_floorq1(mantissa)); i++) {
502+
mantissa = Sleef_mulq1_u05(mantissa, Sleef_cast_from_doubleq1(2.0));
503+
exponent--;
504+
}
505+
506+
507+
// numerator and denominators can't fit in int
508+
// convert items to PyLongObject from string instead
509+
510+
PyObject *py_exp = PyLong_FromLongLong(Py_ABS(exponent));
511+
if(py_exp == NULL)
512+
{
513+
return NULL;
514+
}
515+
516+
PyObject *numerator = quad_to_pylong(mantissa);
517+
if(numerator == NULL)
518+
{
519+
Py_DECREF(numerator);
520+
return NULL;
521+
}
522+
PyObject *denominator = PyLong_FromLong(1);
523+
if (denominator == NULL) {
524+
Py_DECREF(numerator);
525+
return NULL;
526+
}
527+
528+
// fold in 2**exponent
529+
if(exponent > 0)
530+
{
531+
PyObject *new_num = PyNumber_Lshift(numerator, py_exp);
532+
Py_DECREF(numerator);
533+
if(new_num == NULL)
534+
{
535+
Py_DECREF(denominator);
536+
Py_DECREF(py_exp);
537+
return NULL;
538+
}
539+
numerator = new_num;
540+
}
541+
else
542+
{
543+
PyObject *new_denom = PyNumber_Lshift(denominator, py_exp);
544+
Py_DECREF(denominator);
545+
if(new_denom == NULL)
546+
{
547+
Py_DECREF(numerator);
548+
Py_DECREF(py_exp);
549+
return NULL;
550+
}
551+
denominator = new_denom;
552+
}
553+
554+
Py_DECREF(py_exp);
555+
return PyTuple_Pack(2, numerator, denominator);
429556
}
430557

431558
static PyMethodDef QuadPrecision_methods[] = {

quaddtype/tests/test_quaddtype.py

Lines changed: 71 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -3552,105 +3552,74 @@ def test_is_integer_compatibility_with_float(self, value):
35523552
float_val = float(value)
35533553
assert quad_val.is_integer() == float_val.is_integer()
35543554

3555-
# @pytest.mark.parametrize("value,expected_num,expected_denom", [
3556-
# ("1.0", 1, 1),
3557-
# ("42.0", 42, 1),
3558-
# ("-5.0", -5, 1),
3559-
# ("0.0", 0, 1),
3560-
# ])
3561-
# def test_as_integer_ratio_integers(self, value, expected_num, expected_denom):
3562-
# """Test as_integer_ratio() for integer values."""
3563-
# num, denom = QuadPrecision(value).as_integer_ratio()
3564-
# assert num == expected_num and denom == expected_denom
3565-
3566-
# @pytest.mark.parametrize("value,expected_ratio", [
3567-
# ("0.5", 0.5),
3568-
# ("0.25", 0.25),
3569-
# ("1.5", 1.5),
3570-
# ("-2.5", -2.5),
3571-
# ])
3572-
# def test_as_integer_ratio_fractional(self, value, expected_ratio):
3573-
# """Test as_integer_ratio() for fractional values."""
3574-
# num, denom = QuadPrecision(value).as_integer_ratio()
3575-
# assert num / denom == expected_ratio
3576-
# assert denom > 0 # Denominator should always be positive
3577-
3578-
# @pytest.mark.parametrize("value", [
3579-
# "3.14", "0.1", "1.414213562373095", "2.718281828459045",
3580-
# "-1.23456789", "1000.001", "0.0001", "1e20", "1.23e15", "1e-30"
3581-
# ])
3582-
# def test_as_integer_ratio_reconstruction(self, value):
3583-
# """Test that as_integer_ratio() can reconstruct the original value."""
3584-
# quad_val = QuadPrecision(value)
3585-
# num, denom = quad_val.as_integer_ratio()
3586-
# reconstructed = QuadPrecision(num) / QuadPrecision(denom)
3587-
# assert reconstructed == quad_val
3588-
3589-
# def test_as_integer_ratio_return_types(self):
3590-
# """Test that as_integer_ratio() returns Python ints."""
3591-
# num, denom = QuadPrecision("3.14").as_integer_ratio()
3592-
# assert isinstance(num, int)
3593-
# assert isinstance(denom, int)
3594-
3595-
# @pytest.mark.parametrize("value", ["-1.0", "-3.14", "-0.5", "1.0", "3.14", "0.5"])
3596-
# def test_as_integer_ratio_denominator_positive(self, value):
3597-
# """Test that denominator is always positive."""
3598-
# num, denom = QuadPrecision(value).as_integer_ratio()
3599-
# assert denom > 0
3600-
3601-
# @pytest.mark.parametrize("value,exception,match", [
3602-
# ("inf", OverflowError, "cannot convert Infinity to integer ratio"),
3603-
# ("-inf", OverflowError, "cannot convert Infinity to integer ratio"),
3604-
# ("nan", ValueError, "cannot convert NaN to integer ratio"),
3605-
# ])
3606-
# def test_as_integer_ratio_special_values_raise(self, value, exception, match):
3607-
# """Test that as_integer_ratio() raises appropriate errors for special values."""
3608-
# with pytest.raises(exception, match=match):
3609-
# QuadPrecision(value).as_integer_ratio()
3610-
3611-
# @pytest.mark.parametrize("value", ["1.0", "0.5", "3.14", "-2.5", "0.0"])
3612-
# def test_as_integer_ratio_compatibility_with_float(self, value):
3613-
# """Test as_integer_ratio() matches behavior of Python's float where possible."""
3614-
# quad_val = QuadPrecision(value)
3615-
# float_val = float(value)
3616-
3617-
# quad_num, quad_denom = quad_val.as_integer_ratio()
3618-
# float_num, float_denom = float_val.as_integer_ratio()
3619-
3620-
# # The ratios should be equal
3621-
# quad_ratio = quad_num / quad_denom
3622-
# float_ratio = float_num / float_denom
3623-
# assert abs(quad_ratio - float_ratio) < 1e-15
3624-
3625-
# def test_methods_available_on_type_and_instance(self):
3626-
# """Test that methods are available on the QuadPrecision class and instances."""
3627-
# # Check on type
3628-
# assert hasattr(QuadPrecision, 'is_integer') and callable(QuadPrecision.is_integer)
3629-
# assert hasattr(QuadPrecision, 'as_integer_ratio') and callable(QuadPrecision.as_integer_ratio)
3630-
3631-
# # Check on instance
3632-
# val = QuadPrecision("3.14")
3633-
# assert hasattr(val, 'is_integer') and callable(val.is_integer)
3634-
# assert hasattr(val, 'as_integer_ratio') and callable(val.as_integer_ratio)
3635-
3636-
# @pytest.mark.parametrize("backend", ["sleef", "longdouble"])
3637-
# def test_is_integer_with_backends(self, backend):
3638-
# """Test is_integer() with different backends."""
3639-
# if backend == "longdouble" and not numpy_quaddtype.is_longdouble_128():
3640-
# pytest.skip("longdouble backend not 128-bit")
3641-
3642-
# val = QuadPrecision("3.0", backend=backend)
3643-
# assert val.is_integer()
3644-
3645-
# val2 = QuadPrecision("3.5", backend=backend)
3646-
# assert not val2.is_integer()
3647-
3648-
# @pytest.mark.parametrize("backend", ["sleef", "longdouble"])
3649-
# def test_as_integer_ratio_with_backends(self, backend):
3650-
# """Test as_integer_ratio() with different backends."""
3651-
# if backend == "longdouble" and not numpy_quaddtype.is_longdouble_128():
3652-
# pytest.skip("longdouble backend not 128-bit")
3653-
3654-
# val = QuadPrecision("1.5", backend=backend)
3655-
# num, denom = val.as_integer_ratio()
3656-
# assert num / denom == 1.5
3555+
@pytest.mark.parametrize("value,expected_num,expected_denom", [
3556+
("1.0", 1, 1),
3557+
("42.0", 42, 1),
3558+
("-5.0", -5, 1),
3559+
("0.0", 0, 1),
3560+
("-0.0", 0, 1),
3561+
])
3562+
def test_as_integer_ratio_integers(self, value, expected_num, expected_denom):
3563+
"""Test as_integer_ratio() for integer values."""
3564+
num, denom = QuadPrecision(value).as_integer_ratio()
3565+
assert num == expected_num and denom == expected_denom
3566+
3567+
@pytest.mark.parametrize("value,expected_ratio", [
3568+
("0.5", 0.5),
3569+
("0.25", 0.25),
3570+
("1.5", 1.5),
3571+
("-2.5", -2.5),
3572+
])
3573+
def test_as_integer_ratio_fractional(self, value, expected_ratio):
3574+
"""Test as_integer_ratio() for fractional values."""
3575+
num, denom = QuadPrecision(value).as_integer_ratio()
3576+
assert QuadPrecision(str(num)) / QuadPrecision(str(denom)) == QuadPrecision(str(expected_ratio))
3577+
assert denom > 0 # Denominator should always be positive
3578+
3579+
@pytest.mark.parametrize("value", [
3580+
"3.14", "0.1", "1.414213562373095", "2.718281828459045",
3581+
"-1.23456789", "1000.001", "0.0001", "1e20", "1.23e15", "1e-30", quad_pi
3582+
])
3583+
def test_as_integer_ratio_reconstruction(self, value):
3584+
"""Test that as_integer_ratio() can reconstruct the original value."""
3585+
quad_val = QuadPrecision(value)
3586+
num, denom = quad_val.as_integer_ratio()
3587+
# todo: can remove str converstion after merging PR #213
3588+
reconstructed = QuadPrecision(str(num)) / QuadPrecision(str(denom))
3589+
assert reconstructed == quad_val
3590+
3591+
def test_as_integer_ratio_return_types(self):
3592+
"""Test that as_integer_ratio() returns Python ints."""
3593+
num, denom = QuadPrecision("3.14").as_integer_ratio()
3594+
assert isinstance(num, int)
3595+
assert isinstance(denom, int)
3596+
3597+
@pytest.mark.parametrize("value", ["-1.0", "-3.14", "-0.5", "1.0", "3.14", "0.5"])
3598+
def test_as_integer_ratio_denominator_positive(self, value):
3599+
"""Test that denominator is always positive."""
3600+
num, denom = QuadPrecision(value).as_integer_ratio()
3601+
assert denom > 0
3602+
3603+
@pytest.mark.parametrize("value,exception,match", [
3604+
("inf", OverflowError, "Cannot convert infinite value to integer ratio"),
3605+
("-inf", OverflowError, "Cannot convert infinite value to integer ratio"),
3606+
("nan", ValueError, "Cannot convert NaN to integer ratio"),
3607+
])
3608+
def test_as_integer_ratio_special_values_raise(self, value, exception, match):
3609+
"""Test that as_integer_ratio() raises appropriate errors for special values."""
3610+
with pytest.raises(exception, match=match):
3611+
QuadPrecision(value).as_integer_ratio()
3612+
3613+
@pytest.mark.parametrize("value", ["1.0", "0.5", "3.14", "-2.5", "0.0"])
3614+
def test_as_integer_ratio_compatibility_with_float(self, value):
3615+
"""Test as_integer_ratio() matches behavior of Python's float where possible."""
3616+
quad_val = QuadPrecision(value)
3617+
float_val = float(value)
3618+
3619+
quad_num, quad_denom = quad_val.as_integer_ratio()
3620+
float_num, float_denom = float_val.as_integer_ratio()
3621+
3622+
# The ratios should be equal
3623+
quad_ratio = quad_num / quad_denom
3624+
float_ratio = float_num / float_denom
3625+
assert abs(quad_ratio - float_ratio) < 1e-15

0 commit comments

Comments
 (0)