diff --git a/quaddtype/numpy_quaddtype/src/casts.cpp b/quaddtype/numpy_quaddtype/src/casts.cpp index e50ba7ee..034586a6 100644 --- a/quaddtype/numpy_quaddtype/src/casts.cpp +++ b/quaddtype/numpy_quaddtype/src/casts.cpp @@ -1,7 +1,7 @@ #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define PY_UFUNC_UNIQUE_SYMBOL QuadPrecType_UFUNC_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION -#define NPY_TARGET_VERSION NPY_2_0_API_VERSION +#define NPY_TARGET_VERSION NPY_2_4_API_VERSION #define NO_IMPORT_ARRAY #define NO_IMPORT_UFUNC @@ -15,6 +15,7 @@ extern "C" { } #include #include +#include #include "sleef.h" #include "sleefquad.h" @@ -26,10 +27,15 @@ extern "C" { #include "lock.h" #include "dragon4.h" #include "ops.hpp" +#include "constants.hpp" #define NUM_CASTS 40 // 18 to_casts + 18 from_casts + 1 quad_to_quad + 1 void_to_quad #define QUAD_STR_WIDTH 50 // 42 is enough for scientific notation float128, just keeping some buffer +// forward declarations +static inline const char * +quad_to_string_adaptive_cstr(Sleef_quad *sleef_val, npy_intp unicode_size_chars); + static NPY_CASTING quad_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTypeMeta *NPY_UNUSED(dtypes[2]), @@ -54,14 +60,75 @@ quad_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self), *view_offset = NPY_MIN_INTP; if (given_descrs[0]->backend == BACKEND_SLEEF) { // SLEEF -> long double may lose precision - return NPY_SAME_KIND_CASTING; + return static_cast(NPY_SAME_KIND_CASTING | NPY_SAME_VALUE_CASTING_FLAG); } // long double -> SLEEF preserves value exactly - return NPY_SAFE_CASTING; + return static_cast(NPY_SAFE_CASTING | NPY_SAME_VALUE_CASTING_FLAG); } *view_offset = 0; - return NPY_NO_CASTING; + return static_cast(NPY_NO_CASTING | NPY_SAME_VALUE_CASTING_FLAG); +} + +// Helper function for quad-to-quad same_value check (inter-backend) +// NOTE: the inter-backend uses `double` as intermediate, +// so only values that can be exactly represented in double can pass same_value check +static inline int +quad_to_quad_same_value_check(const quad_value *in_val, QuadBackendType backend_in, + const quad_value *out_val, QuadBackendType backend_out) +{ + // Convert output back to input backend for comparison + quad_value roundtrip; + + if (backend_in == BACKEND_SLEEF) { + // Input was SLEEF, output is longdouble + // Convert longdouble back to SLEEF for comparison + long double ld = out_val->longdouble_value; + if (std::isnan(ld)) { + // Preserve sign of NaN + roundtrip.sleef_value = (!ld_signbit(&ld)) ? QUAD_PRECISION_NAN : QUAD_PRECISION_NEG_NAN; + } + else if (std::isinf(ld)) { + roundtrip.sleef_value = (ld > 0) ? QUAD_PRECISION_INF : QUAD_PRECISION_NINF; + } + else { + Sleef_quad temp = Sleef_cast_from_doubleq1(static_cast(ld)); + memcpy(&roundtrip.sleef_value, &temp, sizeof(Sleef_quad)); + } + + // Compare in SLEEF domain && signbit preserved + bool is_sign_preserved = (quad_signbit(&in_val->sleef_value) == quad_signbit(&roundtrip.sleef_value)); + if(quad_isnan(&in_val->sleef_value) && quad_isnan(&roundtrip.sleef_value) && is_sign_preserved) + return 1; // Both NaN + if (Sleef_icmpeqq1(in_val->sleef_value, roundtrip.sleef_value) && is_sign_preserved) + return 1; // Equal + } + else { + // Input was longdouble, output is SLEEF + // Convert SLEEF back to longdouble for comparison + roundtrip.longdouble_value = static_cast(cast_sleef_to_double(out_val->sleef_value)); + + // Compare in longdouble domain && signbit preserved + bool is_sign_preserved = (ld_signbit(&in_val->longdouble_value) == ld_signbit(&roundtrip.longdouble_value)); + if ((std::isnan(in_val->longdouble_value) && std::isnan(roundtrip.longdouble_value)) && is_sign_preserved) + return 1; + if ((in_val->longdouble_value == roundtrip.longdouble_value) && is_sign_preserved) + return 1; + } + + // Values don't match + Sleef_quad sleef_val = quad_to_sleef_quad(in_val, backend_in); + const char *val_str = quad_to_string_adaptive_cstr(&sleef_val, QUAD_STR_WIDTH); + if (val_str != NULL) { + PyErr_Format(PyExc_ValueError, + "QuadPrecision value '%s' cannot be represented exactly in target backend", + val_str); + } + else { + PyErr_SetString(PyExc_ValueError, + "QuadPrecision value cannot be represented exactly in target backend"); + } + return -1; } template @@ -80,6 +147,7 @@ quad_to_quad_strided_loop(PyArrayMethod_Context *context, char *const data[], QuadPrecDTypeObject *descr_out = (QuadPrecDTypeObject *)context->descriptors[1]; QuadBackendType backend_in = descr_in->backend; QuadBackendType backend_out = descr_out->backend; + int same_value_casting = ((context->flags & NPY_SAME_VALUE_CONTEXT_FLAG) == NPY_SAME_VALUE_CONTEXT_FLAG); // inter-backend casting if (backend_in != backend_out) { @@ -108,7 +176,16 @@ quad_to_quad_strided_loop(PyArrayMethod_Context *context, char *const data[], std::memcpy(&out_val.sleef_value, &temp, sizeof(Sleef_quad)); } } - + + // check same_value for inter-backend casts + if(same_value_casting) + { + int ret = quad_to_quad_same_value_check(&in_val, backend_in, &out_val, backend_out); + if (ret < 0) { + return -1; + } + } + store_quad(out_ptr, &out_val, backend_out); in_ptr += in_stride; out_ptr += out_stride; @@ -128,7 +205,6 @@ quad_to_quad_strided_loop(PyArrayMethod_Context *context, char *const data[], return 0; } - static NPY_CASTING void_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTypeMeta *dtypes[2], PyArray_Descr *given_descrs[2], PyArray_Descr *loop_descrs[2], @@ -176,7 +252,7 @@ unicode_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTypeMet loop_descrs[1] = given_descrs[1]; } - return NPY_UNSAFE_CASTING; + return static_cast(NPY_UNSAFE_CASTING | NPY_SAME_VALUE_CASTING_FLAG); } // Helper function: Convert UCS4 string to quad_value @@ -300,42 +376,9 @@ quad_to_unicode_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTypeMet // If target descriptor is wide enough, it's a safe cast if (loop_descrs[1]->elsize >= required_size_bytes) { - return NPY_SAFE_CASTING; - } - return NPY_SAME_KIND_CASTING; -} - -// Helper function: Convert quad to string with adaptive notation -static inline PyObject * -quad_to_string_adaptive(Sleef_quad *sleef_val, npy_intp unicode_size_chars) -{ - // Try positional format first to see if it would fit - PyObject *positional_str = Dragon4_Positional_QuadDType( - sleef_val, DigitMode_Unique, CutoffMode_TotalLength, SLEEF_QUAD_DECIMAL_DIG, 0, 1, - TrimMode_LeaveOneZero, 1, 0); - - if (positional_str == NULL) { - return NULL; - } - - const char *pos_str = PyUnicode_AsUTF8(positional_str); - if (pos_str == NULL) { - Py_DECREF(positional_str); - return NULL; - } - - // no need to scan full, only checking if its longer - npy_intp pos_len = strnlen(pos_str, unicode_size_chars + 1); - - // If positional format fits, use it; otherwise use scientific notation - if (pos_len <= unicode_size_chars) { - return positional_str; // Keep the positional string + return static_cast(NPY_SAFE_CASTING | NPY_SAME_VALUE_CASTING_FLAG); } - Py_DECREF(positional_str); - // Use scientific notation with full precision - return Dragon4_Scientific_QuadDType(sleef_val, DigitMode_Unique, - SLEEF_QUAD_DECIMAL_DIG, 0, 1, - TrimMode_LeaveOneZero, 1, 2); + return static_cast(NPY_SAME_KIND_CASTING | NPY_SAME_VALUE_CASTING_FLAG); } static inline const char * @@ -371,6 +414,65 @@ quad_to_string_adaptive_cstr(Sleef_quad *sleef_val, npy_intp unicode_size_chars) } +static inline int +quad_to_string_same_value_check(const quad_value *in_val, const char *str_buf, npy_intp str_len, + QuadBackendType backend) +{ + char *truncated_str = (char *)malloc(str_len + 1); + if (truncated_str == NULL) { + PyErr_NoMemory(); + return -1; + } + memcpy(truncated_str, str_buf, str_len); + truncated_str[str_len] = '\0'; + + // Parse the truncated string back to quad + quad_value roundtrip; + char *endptr; + + int err = NumPyOS_ascii_strtoq(truncated_str, backend, &roundtrip, &endptr); + if (err < 0) { + PyErr_Format(PyExc_ValueError, + "QuadPrecision value cannot be represented exactly: string '%s' failed to parse back", + truncated_str); + free(truncated_str); + return -1; + } + free(truncated_str); + + // Compare original and roundtripped values along with signbit + if (backend == BACKEND_SLEEF) { + bool is_sign_preserved = (quad_signbit(&in_val->sleef_value) == quad_signbit(&roundtrip.sleef_value)); + if(quad_isnan(&in_val->sleef_value) && quad_isnan(&roundtrip.sleef_value) && is_sign_preserved) + return 1; + if (Sleef_icmpeqq1(in_val->sleef_value, roundtrip.sleef_value) && is_sign_preserved) + return 1; + } + else { + bool is_sign_preserved = (ld_signbit(&in_val->longdouble_value) == ld_signbit(&roundtrip.longdouble_value)); + if ((std::isnan(in_val->longdouble_value) && std::isnan(roundtrip.longdouble_value)) && is_sign_preserved) + return 1; + if ((in_val->longdouble_value == roundtrip.longdouble_value) && is_sign_preserved) + return 1; + } + + // Values don't match - the string width is too narrow for exact representation + Sleef_quad sleef_val = quad_to_sleef_quad(in_val, backend); + const char *val_str = quad_to_string_adaptive_cstr(&sleef_val, QUAD_STR_WIDTH); + if (val_str != NULL) { + PyErr_Format(PyExc_ValueError, + "QuadPrecision value '%s' cannot be represented exactly in target string dtype " + "(string width too narrow or precision loss occurred)", + val_str); + } + else { + PyErr_SetString(PyExc_ValueError, + "QuadPrecision value cannot be represented exactly in target string dtype " + "(string width too narrow or precision loss occurred)"); + } + return -1; +} + template static int quad_to_unicode_loop(PyArrayMethod_Context *context, char *const data[], @@ -388,6 +490,7 @@ quad_to_unicode_loop(PyArrayMethod_Context *context, char *const data[], QuadBackendType backend = descr_in->backend; npy_intp unicode_size_chars = descrs[1]->elsize / 4; + int same_value_casting = ((context->flags & NPY_SAME_VALUE_CONTEXT_FLAG) == NPY_SAME_VALUE_CONTEXT_FLAG); while (N--) { quad_value in_val; @@ -396,21 +499,22 @@ quad_to_unicode_loop(PyArrayMethod_Context *context, char *const data[], // Convert to Sleef_quad for Dragon4 Sleef_quad sleef_val = quad_to_sleef_quad(&in_val, backend); - // Get string representation with adaptive notation - PyObject *py_str = quad_to_string_adaptive(&sleef_val, unicode_size_chars); - if (py_str == NULL) { + const char *temp_str = quad_to_string_adaptive_cstr(&sleef_val, unicode_size_chars); + if (temp_str == NULL) { return -1; } - const char *temp_str = PyUnicode_AsUTF8(py_str); - if (temp_str == NULL) { - Py_DECREF(py_str); - return -1; + npy_intp str_len = strnlen(temp_str, unicode_size_chars); + + // Perform same_value check if requested + if (same_value_casting) { + if (quad_to_string_same_value_check(&in_val, temp_str, str_len, backend) < 0) { + return -1; + } } // Convert char string to UCS4 and store in output Py_UCS4 *out_ucs4 = (Py_UCS4 *)out_ptr; - npy_intp str_len = strnlen(temp_str, unicode_size_chars); for (npy_intp i = 0; i < str_len; i++) { out_ucs4[i] = (Py_UCS4)temp_str[i]; } @@ -418,8 +522,6 @@ quad_to_unicode_loop(PyArrayMethod_Context *context, char *const data[], out_ucs4[i] = 0; } - Py_DECREF(py_str); - in_ptr += in_stride; out_ptr += out_stride; } @@ -449,7 +551,7 @@ bytes_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTypeMeta loop_descrs[1] = given_descrs[1]; } - return NPY_UNSAFE_CASTING; + return static_cast(NPY_UNSAFE_CASTING | NPY_SAME_VALUE_CASTING_FLAG); } // Helper function: Convert bytes string to quad_value @@ -560,9 +662,9 @@ quad_to_bytes_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTypeMeta // If target descriptor is wide enough, it's a safe cast if (loop_descrs[1]->elsize >= required_size_bytes) { - return NPY_SAFE_CASTING; + return static_cast(NPY_SAFE_CASTING | NPY_SAME_VALUE_CASTING_FLAG); } - return NPY_SAME_KIND_CASTING; + return static_cast(NPY_UNSAFE_CASTING | NPY_SAME_VALUE_CASTING_FLAG); } template @@ -582,26 +684,30 @@ quad_to_bytes_loop(PyArrayMethod_Context *context, char *const data[], QuadBackendType backend = descr_in->backend; npy_intp bytes_size = descrs[1]->elsize; + int same_value_casting = ((context->flags & NPY_SAME_VALUE_CONTEXT_FLAG) == NPY_SAME_VALUE_CONTEXT_FLAG); while (N--) { quad_value in_val; load_quad(in_ptr, backend, &in_val); Sleef_quad sleef_val = quad_to_sleef_quad(&in_val, backend); - PyObject *py_str = quad_to_string_adaptive(&sleef_val, bytes_size); - if (py_str == NULL) { - return -1; - } - const char *temp_str = PyUnicode_AsUTF8(py_str); + + const char *temp_str = quad_to_string_adaptive_cstr(&sleef_val, bytes_size); if (temp_str == NULL) { - Py_DECREF(py_str); return -1; } + npy_intp str_len = strnlen(temp_str, bytes_size); + + // Perform same_value check if requested + if (same_value_casting) { + if (quad_to_string_same_value_check(&in_val, temp_str, str_len, backend) < 0) { + return -1; + } + } + // Copy string to output buffer, padding with nulls strncpy(out_ptr, temp_str, bytes_size); - Py_DECREF(py_str); - in_ptr += in_stride; out_ptr += out_stride; } @@ -629,7 +735,7 @@ stringdtype_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp Py_INCREF(given_descrs[0]); loop_descrs[0] = given_descrs[0]; - return NPY_UNSAFE_CASTING; + return static_cast(NPY_UNSAFE_CASTING | NPY_SAME_VALUE_CASTING_FLAG); } // Note: StringDType elements are always aligned, so Aligned template parameter @@ -713,7 +819,7 @@ quad_to_stringdtype_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp Py_INCREF(given_descrs[0]); loop_descrs[0] = given_descrs[0]; - return NPY_SAFE_CASTING; + return static_cast(NPY_SAFE_CASTING | NPY_SAME_VALUE_CASTING_FLAG); } // Note: StringDType elements are always aligned, so Aligned template parameter @@ -734,6 +840,7 @@ quad_to_stringdtype_strided_loop(PyArrayMethod_Context *context, char *const dat QuadPrecDTypeObject *descr_in = (QuadPrecDTypeObject *)descrs[0]; PyArray_StringDTypeObject *str_descr = (PyArray_StringDTypeObject *)descrs[1]; QuadBackendType backend = descr_in->backend; + int same_value_casting = ((context->flags & NPY_SAME_VALUE_CONTEXT_FLAG) == NPY_SAME_VALUE_CONTEXT_FLAG); npy_string_allocator *allocator = NpyString_acquire_allocator(str_descr); @@ -752,6 +859,14 @@ quad_to_stringdtype_strided_loop(PyArrayMethod_Context *context, char *const dat Py_ssize_t str_size = strnlen(str_buf, QUAD_STR_WIDTH); + // Perform same_value check if requested + if (same_value_casting) { + if (quad_to_string_same_value_check(&in_val, str_buf, str_size, backend) < 0) { + NpyString_release_allocator(allocator); + return -1; + } + } + npy_packed_static_string *out_ps = (npy_packed_static_string *)out_ptr; if (NpyString_pack(allocator, out_ps, str_buf, (size_t)str_size) < 0) { NpyString_release_allocator(allocator); @@ -1060,42 +1175,15 @@ numpy_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTypeMeta } loop_descrs[0] = PyArray_GetDefaultDescr(dtypes[0]); - return NPY_SAFE_CASTING; + // since QUAD precision is the highest precision, we can always cast to it + return static_cast(NPY_SAFE_CASTING | NPY_SAME_VALUE_CASTING_FLAG); } -template +template static int -numpy_to_quad_strided_loop_unaligned(PyArrayMethod_Context *context, char *const data[], - npy_intp const dimensions[], npy_intp const strides[], - void *NPY_UNUSED(auxdata)) -{ - npy_intp N = dimensions[0]; - char *in_ptr = data[0]; - char *out_ptr = data[1]; - - QuadPrecDTypeObject *descr_out = (QuadPrecDTypeObject *)context->descriptors[1]; - QuadBackendType backend = descr_out->backend; - size_t elem_size = (backend == BACKEND_SLEEF) ? sizeof(Sleef_quad) : sizeof(long double); - - while (N--) { - typename NpyType::TYPE in_val; - quad_value out_val; - - memcpy(&in_val, in_ptr, sizeof(typename NpyType::TYPE)); - out_val = to_quad(in_val, backend); - memcpy(out_ptr, &out_val, elem_size); - - in_ptr += strides[0]; - out_ptr += strides[1]; - } - return 0; -} - -template -static int -numpy_to_quad_strided_loop_aligned(PyArrayMethod_Context *context, char *const data[], - npy_intp const dimensions[], npy_intp const strides[], - void *NPY_UNUSED(auxdata)) +numpy_to_quad_strided_loop(PyArrayMethod_Context *context, char *const data[], + npy_intp const dimensions[], npy_intp const strides[], + void *NPY_UNUSED(auxdata)) { npy_intp N = dimensions[0]; char *in_ptr = data[0]; @@ -1105,15 +1193,9 @@ numpy_to_quad_strided_loop_aligned(PyArrayMethod_Context *context, char *const d QuadBackendType backend = descr_out->backend; while (N--) { - typename NpyType::TYPE in_val = *(typename NpyType::TYPE *)in_ptr; + typename NpyType::TYPE in_val = load::TYPE>(in_ptr); quad_value out_val = to_quad(in_val, backend); - - if (backend == BACKEND_SLEEF) { - *(Sleef_quad *)(out_ptr) = out_val.sleef_value; - } - else { - *(long double *)(out_ptr) = out_val.longdouble_value; - } + store_quad(out_ptr, &out_val, backend); in_ptr += strides[0]; out_ptr += strides[1]; @@ -1310,6 +1392,56 @@ from_quad(const quad_value *x, QuadBackendType backend) } } +template +static inline int quad_to_numpy_same_value_check(const quad_value *x, QuadBackendType backend, typename NpyType::TYPE *y) +{ + *y = from_quad(x, backend); + quad_value roundtrip = to_quad(*y, backend); + if(backend == BACKEND_SLEEF) + { + bool is_sign_preserved = (quad_signbit(&x->sleef_value) == quad_signbit(&roundtrip.sleef_value)); + // check if input is NaN and roundtrip is NaN with same sign + if(quad_isnan(&x->sleef_value) && quad_isnan(&roundtrip.sleef_value) && is_sign_preserved) + return 1; + if(Sleef_icmpeqq1(x->sleef_value, roundtrip.sleef_value) && is_sign_preserved) + return 1; + } + else + { + bool is_sign_preserved = (ld_signbit(&x->longdouble_value) == ld_signbit(&roundtrip.longdouble_value)); + if((std::isnan(x->longdouble_value) && std::isnan(roundtrip.longdouble_value)) && is_sign_preserved) + return 1; + if((x->longdouble_value == roundtrip.longdouble_value) && is_sign_preserved) + return 1; + + } + Sleef_quad sleef_val = quad_to_sleef_quad(x, backend); + const char *val_str = quad_to_string_adaptive_cstr(&sleef_val, QUAD_STR_WIDTH); + if (val_str != NULL) { + PyErr_Format(PyExc_ValueError, + "QuadPrecision value '%s' cannot be represented exactly in the target dtype", + val_str); + } + else { + PyErr_SetString(PyExc_ValueError, + "QuadPrecision value cannot be represented exactly in the target dtype"); + } + return -1; +} + +// Type trait to check if a type is a floating-point type for casting purposes +template +struct is_float_type : std::false_type {}; + +template <> +struct is_float_type : std::true_type {}; +template <> +struct is_float_type : std::true_type {}; +template <> +struct is_float_type : std::true_type {}; +template <> +struct is_float_type : std::true_type {}; + template static NPY_CASTING quad_to_numpy_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTypeMeta *dtypes[2], @@ -1320,14 +1452,20 @@ quad_to_numpy_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTypeMeta loop_descrs[0] = given_descrs[0]; loop_descrs[1] = PyArray_GetDefaultDescr(dtypes[1]); - return NPY_UNSAFE_CASTING; + // For floating-point types: same_kind casting (precision loss but same kind) + if constexpr (is_float_type::value) { + return static_cast(NPY_SAME_KIND_CASTING | NPY_SAME_VALUE_CASTING_FLAG); + } else { + // For integer/bool types: unsafe casting (cross-kind conversion) + return static_cast(NPY_UNSAFE_CASTING | NPY_SAME_VALUE_CASTING_FLAG); + } } -template +template static int -quad_to_numpy_strided_loop_unaligned(PyArrayMethod_Context *context, char *const data[], - npy_intp const dimensions[], npy_intp const strides[], - void *NPY_UNUSED(auxdata)) +quad_to_numpy_strided_loop(PyArrayMethod_Context *context, char *const data[], + npy_intp const dimensions[], npy_intp const strides[], + void *NPY_UNUSED(auxdata)) { npy_intp N = dimensions[0]; char *in_ptr = data[0]; @@ -1335,46 +1473,28 @@ quad_to_numpy_strided_loop_unaligned(PyArrayMethod_Context *context, char *const QuadPrecDTypeObject *quad_descr = (QuadPrecDTypeObject *)context->descriptors[0]; QuadBackendType backend = quad_descr->backend; + int same_value_casting = ((context->flags & NPY_SAME_VALUE_CONTEXT_FLAG) == NPY_SAME_VALUE_CONTEXT_FLAG); - size_t elem_size = (backend == BACKEND_SLEEF) ? sizeof(Sleef_quad) : sizeof(long double); - - while (N--) { - quad_value in_val; - memcpy(&in_val, in_ptr, elem_size); - - typename NpyType::TYPE out_val = from_quad(&in_val, backend); - memcpy(out_ptr, &out_val, sizeof(typename NpyType::TYPE)); + if (same_value_casting) { + while (N--) { + quad_value in_val; + load_quad(in_ptr, backend, &in_val); + typename NpyType::TYPE out_val; + int ret = quad_to_numpy_same_value_check(&in_val, backend, &out_val); + if(ret < 0) + return -1; + store::TYPE>(out_ptr, out_val); - in_ptr += strides[0]; - out_ptr += strides[1]; + in_ptr += strides[0]; + out_ptr += strides[1]; + } + return 0; } - return 0; -} - -template -static int -quad_to_numpy_strided_loop_aligned(PyArrayMethod_Context *context, char *const data[], - npy_intp const dimensions[], npy_intp const strides[], - void *NPY_UNUSED(auxdata)) -{ - npy_intp N = dimensions[0]; - char *in_ptr = data[0]; - char *out_ptr = data[1]; - - QuadPrecDTypeObject *quad_descr = (QuadPrecDTypeObject *)context->descriptors[0]; - QuadBackendType backend = quad_descr->backend; - while (N--) { quad_value in_val; - if (backend == BACKEND_SLEEF) { - in_val.sleef_value = *(Sleef_quad *)in_ptr; - } - else { - in_val.longdouble_value = *(long double *)in_ptr; - } - + load_quad(in_ptr, backend, &in_val); typename NpyType::TYPE out_val = from_quad(&in_val, backend); - *(typename NpyType::TYPE *)(out_ptr) = out_val; + store::TYPE>(out_ptr, out_val); in_ptr += strides[0]; out_ptr += strides[1]; @@ -1407,8 +1527,8 @@ add_cast_from(PyArray_DTypeMeta *to) PyType_Slot *slots = new PyType_Slot[]{ {NPY_METH_resolve_descriptors, (void *)&quad_to_numpy_resolve_descriptors}, - {NPY_METH_strided_loop, (void *)&quad_to_numpy_strided_loop_aligned}, - {NPY_METH_unaligned_strided_loop, (void *)&quad_to_numpy_strided_loop_unaligned}, + {NPY_METH_strided_loop, (void *)&quad_to_numpy_strided_loop}, + {NPY_METH_unaligned_strided_loop, (void *)&quad_to_numpy_strided_loop}, {0, nullptr}}; PyArrayMethod_Spec *spec = new PyArrayMethod_Spec{ @@ -1431,8 +1551,8 @@ add_cast_to(PyArray_DTypeMeta *from) PyType_Slot *slots = new PyType_Slot[]{ {NPY_METH_resolve_descriptors, (void *)&numpy_to_quad_resolve_descriptors}, - {NPY_METH_strided_loop, (void *)&numpy_to_quad_strided_loop_aligned}, - {NPY_METH_unaligned_strided_loop, (void *)&numpy_to_quad_strided_loop_unaligned}, + {NPY_METH_strided_loop, (void *)&numpy_to_quad_strided_loop}, + {NPY_METH_unaligned_strided_loop, (void *)&numpy_to_quad_strided_loop}, {0, nullptr}}; PyArrayMethod_Spec *spec = new PyArrayMethod_Spec{ diff --git a/quaddtype/numpy_quaddtype/src/dragon4.c b/quaddtype/numpy_quaddtype/src/dragon4.c index 02818207..f86f2697 100644 --- a/quaddtype/numpy_quaddtype/src/dragon4.c +++ b/quaddtype/numpy_quaddtype/src/dragon4.c @@ -17,7 +17,7 @@ Modifications are specific to support the SLEEF_QUAD #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define PY_UFUNC_UNIQUE_SYMBOL QuadPrecType_UFUNC_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION -#define NPY_TARGET_VERSION NPY_2_0_API_VERSION +#define NPY_TARGET_VERSION NPY_2_4_API_VERSION #define NO_IMPORT_ARRAY #define NO_IMPORT_UFUNC diff --git a/quaddtype/numpy_quaddtype/src/dtype.c b/quaddtype/numpy_quaddtype/src/dtype.c index 664a3aa3..910b245e 100644 --- a/quaddtype/numpy_quaddtype/src/dtype.c +++ b/quaddtype/numpy_quaddtype/src/dtype.c @@ -7,7 +7,7 @@ #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define PY_UFUNC_UNIQUE_SYMBOL QuadPrecType_UFUNC_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION -#define NPY_TARGET_VERSION NPY_2_0_API_VERSION +#define NPY_TARGET_VERSION NPY_2_4_API_VERSION #define NO_IMPORT_ARRAY #define NO_IMPORT_UFUNC #include "numpy/arrayobject.h" diff --git a/quaddtype/numpy_quaddtype/src/ops.hpp b/quaddtype/numpy_quaddtype/src/ops.hpp index 6649e7a4..77041a7d 100644 --- a/quaddtype/numpy_quaddtype/src/ops.hpp +++ b/quaddtype/numpy_quaddtype/src/ops.hpp @@ -1669,4 +1669,4 @@ cast_sleef_to_double(const Sleef_quad in) return quad_signbit(&in) ? -0.0 : 0.0; } return Sleef_cast_to_doubleq1(in); -} \ No newline at end of file +} diff --git a/quaddtype/numpy_quaddtype/src/quaddtype_main.c b/quaddtype/numpy_quaddtype/src/quaddtype_main.c index b268077a..4aaa6a51 100644 --- a/quaddtype/numpy_quaddtype/src/quaddtype_main.c +++ b/quaddtype/numpy_quaddtype/src/quaddtype_main.c @@ -6,7 +6,7 @@ #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define PY_UFUNC_UNIQUE_SYMBOL QuadPrecType_UFUNC_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION -#define NPY_TARGET_VERSION NPY_2_0_API_VERSION +#define NPY_TARGET_VERSION NPY_2_4_API_VERSION #include "numpy/arrayobject.h" #include "numpy/dtype_api.h" diff --git a/quaddtype/numpy_quaddtype/src/scalar.c b/quaddtype/numpy_quaddtype/src/scalar.c index 8a63fd00..f1bcddd4 100644 --- a/quaddtype/numpy_quaddtype/src/scalar.c +++ b/quaddtype/numpy_quaddtype/src/scalar.c @@ -104,7 +104,6 @@ QuadPrecision_from_object(PyObject *value, QuadBackendType backend) } double dval = PyFloat_AsDouble(py_float); Py_DECREF(py_float); - if (backend == BACKEND_SLEEF) { self->value.sleef_value = Sleef_cast_from_doubleq1(dval); } diff --git a/quaddtype/numpy_quaddtype/src/scalar_ops.cpp b/quaddtype/numpy_quaddtype/src/scalar_ops.cpp index 3c4a31a3..69a525b4 100644 --- a/quaddtype/numpy_quaddtype/src/scalar_ops.cpp +++ b/quaddtype/numpy_quaddtype/src/scalar_ops.cpp @@ -1,6 +1,6 @@ #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION -#define NPY_TARGET_VERSION NPY_2_0_API_VERSION +#define NPY_TARGET_VERSION NPY_2_4_API_VERSION #define NO_IMPORT_ARRAY extern "C" { @@ -134,6 +134,9 @@ quad_richcompare(QuadPrecisionObject *self, PyObject *other, int cmp_op) Py_INCREF(other); other_quad = (QuadPrecisionObject *)other; if (other_quad->backend != backend) { + // we could allow, but this will be bad + // Two values that are different in quad precision, + // might appear equal when converted to double. PyErr_SetString(PyExc_TypeError, "Cannot compare QuadPrecision objects with different backends"); Py_DECREF(other_quad); diff --git a/quaddtype/numpy_quaddtype/src/umath/binary_ops.cpp b/quaddtype/numpy_quaddtype/src/umath/binary_ops.cpp index 23bd52a0..003da813 100644 --- a/quaddtype/numpy_quaddtype/src/umath/binary_ops.cpp +++ b/quaddtype/numpy_quaddtype/src/umath/binary_ops.cpp @@ -1,7 +1,7 @@ #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define PY_UFUNC_UNIQUE_SYMBOL QuadPrecType_UFUNC_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION -#define NPY_TARGET_VERSION NPY_2_0_API_VERSION +#define NPY_TARGET_VERSION NPY_2_4_API_VERSION #define NO_IMPORT_ARRAY #define NO_IMPORT_UFUNC diff --git a/quaddtype/numpy_quaddtype/src/umath/comparison_ops.cpp b/quaddtype/numpy_quaddtype/src/umath/comparison_ops.cpp index 9cf45071..456323ac 100644 --- a/quaddtype/numpy_quaddtype/src/umath/comparison_ops.cpp +++ b/quaddtype/numpy_quaddtype/src/umath/comparison_ops.cpp @@ -1,7 +1,7 @@ #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define PY_UFUNC_UNIQUE_SYMBOL QuadPrecType_UFUNC_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION -#define NPY_TARGET_VERSION NPY_2_0_API_VERSION +#define NPY_TARGET_VERSION NPY_2_4_API_VERSION #define NO_IMPORT_ARRAY #define NO_IMPORT_UFUNC diff --git a/quaddtype/numpy_quaddtype/src/umath/matmul.cpp b/quaddtype/numpy_quaddtype/src/umath/matmul.cpp index d377e9f4..416afa0a 100644 --- a/quaddtype/numpy_quaddtype/src/umath/matmul.cpp +++ b/quaddtype/numpy_quaddtype/src/umath/matmul.cpp @@ -1,7 +1,7 @@ #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define PY_UFUNC_UNIQUE_SYMBOL QuadPrecType_UFUNC_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION -#define NPY_TARGET_VERSION NPY_2_0_API_VERSION +#define NPY_TARGET_VERSION NPY_2_4_API_VERSION #define NO_IMPORT_ARRAY #define NO_IMPORT_UFUNC diff --git a/quaddtype/numpy_quaddtype/src/umath/umath.cpp b/quaddtype/numpy_quaddtype/src/umath/umath.cpp index a075c4b8..68734b70 100644 --- a/quaddtype/numpy_quaddtype/src/umath/umath.cpp +++ b/quaddtype/numpy_quaddtype/src/umath/umath.cpp @@ -1,7 +1,7 @@ #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define PY_UFUNC_UNIQUE_SYMBOL QuadPrecType_UFUNC_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION -#define NPY_TARGET_VERSION NPY_2_0_API_VERSION +#define NPY_TARGET_VERSION NPY_2_4_API_VERSION #define NO_IMPORT_ARRAY #define NO_IMPORT_UFUNC diff --git a/quaddtype/numpy_quaddtype/src/umath/unary_ops.cpp b/quaddtype/numpy_quaddtype/src/umath/unary_ops.cpp index d30483f3..b83ecb88 100644 --- a/quaddtype/numpy_quaddtype/src/umath/unary_ops.cpp +++ b/quaddtype/numpy_quaddtype/src/umath/unary_ops.cpp @@ -1,7 +1,7 @@ #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define PY_UFUNC_UNIQUE_SYMBOL QuadPrecType_UFUNC_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION -#define NPY_TARGET_VERSION NPY_2_0_API_VERSION +#define NPY_TARGET_VERSION NPY_2_4_API_VERSION #define NO_IMPORT_ARRAY #define NO_IMPORT_UFUNC diff --git a/quaddtype/numpy_quaddtype/src/umath/unary_props.cpp b/quaddtype/numpy_quaddtype/src/umath/unary_props.cpp index 7e399ad4..fd285a0f 100644 --- a/quaddtype/numpy_quaddtype/src/umath/unary_props.cpp +++ b/quaddtype/numpy_quaddtype/src/umath/unary_props.cpp @@ -1,7 +1,7 @@ #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define PY_UFUNC_UNIQUE_SYMBOL QuadPrecType_UFUNC_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION -#define NPY_TARGET_VERSION NPY_2_0_API_VERSION +#define NPY_TARGET_VERSION NPY_2_4_API_VERSION #define NO_IMPORT_ARRAY #define NO_IMPORT_UFUNC diff --git a/quaddtype/numpy_quaddtype/src/utilities.h b/quaddtype/numpy_quaddtype/src/utilities.h index 4697d6eb..6a0384c6 100644 --- a/quaddtype/numpy_quaddtype/src/utilities.h +++ b/quaddtype/numpy_quaddtype/src/utilities.h @@ -63,7 +63,6 @@ template static inline void load_quad(const char *ptr, QuadBackendType backend, quad_value *out) { - quad_value val; if (backend == BACKEND_SLEEF) { out->sleef_value = load(ptr); } diff --git a/quaddtype/tests/test_quaddtype.py b/quaddtype/tests/test_quaddtype.py index 7b73ec11..6d98b797 100644 --- a/quaddtype/tests/test_quaddtype.py +++ b/quaddtype/tests/test_quaddtype.py @@ -5399,6 +5399,273 @@ def test_quad_to_quad_backend_casting(src_backend, dst_backend, value): else: np.testing.assert_array_equal(dst_arr, res_arr) +class TestSameValueCasting: + """Test 'same_value' casting behavior for QuadPrecision.""" + def test_same_value_cast(self): + a = np.arange(30, dtype=np.float32) + # upcasting can never fail + b = a.astype(QuadPrecision, casting='same_value') + c = b.astype(np.float32, casting='same_value') + assert np.all(c == a) + with pytest.raises(ValueError): + (b + 1e22).astype(np.float32, casting='same_value') + + + @pytest.mark.parametrize("dtype,passing,failing", [ + # bool: only 0 and 1 are valid + ("bool", [0, 1], [2, -1, 0.5]), + + # int8: [-128, 127] + ("int8", [-128, 0, 127], [-129, 128, 1.5]), + + # uint8: [0, 255] + ("uint8", [0, 255], [-1, 256, 2.5]), + + # int16: [-32768, 32767] + ("int16", [-32768, 0, 32767], [-32769, 32768, 0.1]), + + # uint16: [0, 65535] + ("uint16", [0, 65535], [-1, 65536]), + + # int32: [-2^31, 2^31-1] + ("int32", [-2**31, 0, 2**31 - 1], [-2**31 - 1, 2**31]), + + # uint32: [0, 2^32-1] + ("uint32", [0, 2**32 - 1], [-1, 2**32]), + + # int64: [-2^63, 2^63-1] + ("int64", [-2**63, 0, 2**63 - 1], [-2**63 - 1, 2**63]), + + # uint64: [0, 2^64-1] + ("uint64", [0, 2**64 - 1], [-1, 2**64]), + ]) + def test_same_value_cast_quad_to_int(self, dtype, passing, failing): + """A 128-bit float can represent all consecutive integers exactly up to 2^113""" + for val in passing: + q = np.array([val], dtype=QuadPrecDType()) + result = q.astype(dtype, casting="same_value") + assert result == val + + for val in failing: + q = np.array([val], dtype=QuadPrecDType()) + with pytest.raises(ValueError): + q.astype(dtype, casting="same_value") + + @pytest.mark.parametrize("dtype", [ + np.float16, np.float32, np.float64, np.longdouble + ]) + @pytest.mark.parametrize("val", [0.0, -0.0, float('inf'), float('-inf'), float('nan'), float("-nan")]) + def test_same_value_cast_floats_special_values(self, dtype, val): + """Test that special floating-point values roundtrip correctly.""" + q = np.array([val], dtype=QuadPrecDType()) + result = q.astype(dtype, casting="same_value") + + assert np.signbit(result) == np.signbit(val), f"Sign bit failed for {dtype} with value {val}" + if np.isnan(val): + assert np.isnan(result), f"NaN failed for {dtype}" + else: + assert result == val, f"{val} failed for {dtype}" + + @pytest.mark.parametrize("dtype", [ + np.float16, np.float32, np.float64, np.longdouble + ]) + def test_same_value_cast_floats_within_range(self, dtype): + """Test values that should roundtrip exactly within dtype's precision.""" + info = np.finfo(dtype) + + # Values that should pass (exactly representable) + passing_values = [ + 1.0, -1.0, 0.5, -0.5, 0.25, -0.25, + 2.0, 4.0, 8.0, # powers of 2 + 2 ** info.nmant, # largest consecutive integer + ] + + # For longdouble on x86-64, info.tiny can be ~3.36e-4932, which is outside + # the double range (~2.2e-308). Since SLEEF backend converts quad <-> longdouble + # via double (Sleef_cast_to/from_doubleq1), values outside double's range + # cannot roundtrip correctly. Use double's tiny for longdouble in this case. + double_info = np.finfo(np.float64) + if dtype == np.longdouble and info.tiny < double_info.tiny: + passing_values.append(double_info.tiny) + else: + passing_values.append(info.tiny) + + for val in passing_values: + # Ensure the value is representable in the target dtype first + target_val = dtype(val) + q = np.array([target_val], dtype=QuadPrecDType()) + result = q.astype(dtype, casting="same_value") + assert result[0] == target_val, f"Value {val} failed for {dtype}" + + + @pytest.mark.parametrize("dtype", [ + np.float16, np.float32, np.float64, np.longdouble + ]) + def test_same_value_cast_floats_precision_loss(self, dtype): + """Test values that cannot be represented exactly and should fail.""" + import sys + from decimal import Decimal, getcontext + + getcontext().prec = 50 # plenty for quad precision + info = np.finfo(dtype) + nmant = info.nmant # 10 for f16, 23 for f32, 52 for f64 + + if dtype == np.longdouble and nmant >= 112: + pytest.skip("longdouble has same precision as quad on this platform") + + # First odd integer beyond exact representability + first_bad_int = 2 ** (nmant + 1) + 1 + # Value between 1.0 and 1.0 + eps (i.e., 1 + eps/2) + # eps = 2^-nmant, so eps/2 = 2^-(nmant+1) + one_plus_half_eps = Decimal(1) + Decimal(2) ** -(nmant + 1) + + # Value between 2.0 and 2.0 + 2*eps + two_plus_eps = Decimal(2) + Decimal(2) ** -nmant + + failing_values = [ + str(first_bad_int), + str(one_plus_half_eps), + str(two_plus_eps), + ] + + for val in failing_values: + q = np.array([val], dtype=QuadPrecDType()) + with pytest.raises(ValueError): + q.astype(dtype, casting="same_value") + + @pytest.mark.parametrize("dtype", [ + "S50", "U50", "S100", "U100", np.dtypes.StringDType() + ]) + def test_same_value_cast_strings_enough_width(self, dtype): + """Test that string types with enough width can represent quad values exactly.""" + values = [ + "0.0", "-0.0", "1.0", "-1.0", + "3.14159265358979323846264338327950288", # pi with full quad precision + "inf", "-inf", "nan", "-nan", + "1.23e100", "-4.56e-100", + ] + + for val in values: + q = np.array([val], dtype=QuadPrecDType()) + result = q.astype(dtype, casting="same_value") + # Convert back and verify + back = result.astype(QuadPrecDType()) + assert np.signbit(back[0]) == np.signbit(q[0]), f"Sign bit roundtrip failed for {dtype} with value {val}" + if np.isnan(q[0]): + assert np.isnan(back[0]), f"NaN roundtrip failed for {dtype}" + else: + assert q[0] == back[0], f"Value {val} roundtrip failed for {dtype}" + + @pytest.mark.parametrize("dtype", ["S10", "U10"]) + def test_same_value_cast_strings_narrow_width(self, dtype): + """Test that string types with narrow width fail for values that need more precision.""" + # Values that can fit in 10 chars should pass + passing_values = ["0.0", "-0.0", "1.0", "-1.0", "inf", "-inf", "nan", "-nan"] + for val in passing_values: + q = np.array([val], dtype=QuadPrecDType()) + result = q.astype(dtype, casting="same_value") + back = result.astype(QuadPrecDType()) + assert np.signbit(back[0]) == np.signbit(q[0]), f"Sign bit roundtrip failed for {dtype} with value {val}" + if np.isnan(q[0]): + assert np.isnan(back[0]) + else: + assert q[0] == back[0], f"Value {val} should roundtrip in {dtype}" + + # Values that need more than 10 chars should fail + failing_values = [ + "3.14159265358979323846264338327950288", # pi + "1.23456789012345", # needs > 10 chars + ] + for val in failing_values: + q = np.array([val], dtype=QuadPrecDType()) + with pytest.raises(ValueError): + q.astype(dtype, casting="same_value") + + @pytest.mark.parametrize("src_backend,dst_backend", [ + ("sleef", "longdouble"), + ("longdouble", "sleef"), + ("sleef", "sleef"), + ("longdouble", "longdouble") + ]) + def test_quad_to_quad_same_value_casting_passing(self, src_backend, dst_backend): + """Test values that should roundtrip exactly between backends.""" + # Values exactly representable in both backends (and in double, since + # inter-backend conversion goes through double) + passing_values = [ + 0.0, -0.0, 1.0, -1.0, + 0.5, 0.25, 0.125, + 2.0, 4.0, 8.0, + "inf", "-inf", "nan", "-nan", + 1e100, -1e-100, + str(2**52), # Largest consecutive integer in double + ] + + for val in passing_values: + src = np.array([val], dtype=QuadPrecDType(backend=src_backend)) + result = src.astype(QuadPrecDType(backend=dst_backend), casting="same_value") + + # Verify value is preserved + assert np.signbit(result[0]) == np.signbit(src[0]), f"Sign bit failed for {val} in {src_backend} -> {dst_backend}" + if val in ["nan", "-nan"] : + assert np.isnan(result[0]) + else: + # compare them as float, as these values anyhow have to under double's range to work + assert float(result[0]) == float(src[0]), f"Value {val} failed for {src_backend} -> {dst_backend}" + + + @pytest.mark.parametrize("src_backend,dst_backend", [ + ("sleef", "longdouble"), + ("longdouble", "sleef"), + ]) + def test_quad_to_quad_interbackend_same_value_casting_failing(self, src_backend, dst_backend): + """Test values that cannot roundtrip exactly between backends. + + Inter-backend conversion goes through double, so values exceeding + double's precision (~53 bits mantissa) will fail same_value casting. + """ + ld_info = np.finfo(np.longdouble) + double_info = np.finfo(np.float64) + + # Skip if longdouble has same precision as quad (PowerPC binary128) + # In that case, sleef <-> longdouble might use a direct path + if ld_info.nmant >= 112: + pytest.skip("longdouble has same precision as quad on this platform") + + # For longdouble -> sleef: only fails if longdouble has more precision than double + if src_backend == "longdouble" and ld_info.nmant <= double_info.nmant: + pytest.skip("longdouble has same or less precision than double on this platform") + + # Values that exceed double precision (53-bit mantissa) + # These will lose precision when going through the double conversion + failing_values = [ + str(2**53 + 1), # First integer not exactly representable in double + "3.141592653589793238462643383279502884197", # Pi with more than double precision + "1.00000000000000011", # 1 + epsilon beyond double precision + ] + + for val in failing_values: + src = np.array([val], dtype=QuadPrecDType(backend=src_backend)) + with pytest.raises(ValueError): + src.astype(QuadPrecDType(backend=dst_backend), casting="same_value") + + + @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + def test_quad_to_quad_same_backend_always_passes(self, backend): + """Same backend conversion should always pass same_value.""" + # Even high-precision values should pass when backend is the same + values = [ + "3.141592653589793238462643383279502884197", + "2.718281828459045235360287471352662497757", + str(2**113), # Large integer + "1e4000", # Large exponent (within quad range) + ] + + for val in values: + src = np.array([val], dtype=QuadPrecDType(backend=backend)) + result = src.astype(QuadPrecDType(backend=backend), casting="same_value") + # Should not raise, and value should be unchanged + assert str(result[0]) == str(src[0]) + # quad -> float will be tested in same_values tests @pytest.mark.parametrize("dtype", [np.float16, np.float32, np.float64, np.longdouble]) @pytest.mark.parametrize("val", [0.0, -0.0, float('inf'), float('-inf'), float('nan'), float("-nan")])