Skip to content

Commit 00acaca

Browse files
committed
quad2quad same_value
1 parent 308f136 commit 00acaca

3 files changed

Lines changed: 146 additions & 81 deletions

File tree

quaddtype/numpy_quaddtype/src/casts.cpp

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ extern "C" {
3232
#define NUM_CASTS 40 // 18 to_casts + 18 from_casts + 1 quad_to_quad + 1 void_to_quad
3333
#define QUAD_STR_WIDTH 50 // 42 is enough for scientific notation float128, just keeping some buffer
3434

35+
// forward declarations
36+
static inline const char *
37+
quad_to_string_adaptive_cstr(Sleef_quad *sleef_val, npy_intp unicode_size_chars);
38+
3539
static NPY_CASTING
3640
quad_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self),
3741
PyArray_DTypeMeta *NPY_UNUSED(dtypes[2]),
@@ -63,15 +67,67 @@ quad_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self),
6367
}
6468

6569
*view_offset = 0;
66-
return NPY_NO_CASTING;
70+
return static_cast<NPY_CASTING>(NPY_NO_CASTING | NPY_SAME_VALUE_CASTING_FLAG);
6771
}
6872

73+
// Helper function for quad-to-quad same_value check (inter-backend)
6974
static inline int
7075
quad_to_quad_same_value_check(const quad_value *in_val, QuadBackendType backend_in,
7176
const quad_value *out_val, QuadBackendType backend_out)
7277
{
73-
// convert output back to input backend for comparison
74-
return 1;
78+
// Convert output back to input backend for comparison
79+
quad_value roundtrip;
80+
81+
if (backend_in == BACKEND_SLEEF) {
82+
// Input was SLEEF, output is longdouble
83+
// Convert longdouble back to SLEEF for comparison
84+
long double ld = out_val->longdouble_value;
85+
if (std::isnan(ld)) {
86+
roundtrip.sleef_value = QUAD_PRECISION_NAN;
87+
}
88+
else if (std::isinf(ld)) {
89+
roundtrip.sleef_value = (ld > 0) ? QUAD_PRECISION_INF : QUAD_PRECISION_NINF;
90+
}
91+
else {
92+
Sleef_quad temp = Sleef_cast_from_doubleq1(static_cast<double>(ld));
93+
memcpy(&roundtrip.sleef_value, &temp, sizeof(Sleef_quad));
94+
}
95+
96+
// Compare in SLEEF domain
97+
if (Sleef_iunordq1(in_val->sleef_value, roundtrip.sleef_value))
98+
return 1; // Both NaN
99+
if (Sleef_icmpeqq1(in_val->sleef_value, roundtrip.sleef_value))
100+
return 1; // Equal
101+
if (Sleef_icmpeqq1(in_val->sleef_value, QUAD_ZERO) && Sleef_icmpeqq1(roundtrip.sleef_value, QUAD_ZERO))
102+
return 1; // Both zeros
103+
}
104+
else {
105+
// Input was longdouble, output is SLEEF
106+
// Convert SLEEF back to longdouble for comparison
107+
roundtrip.longdouble_value = static_cast<long double>(Sleef_cast_to_doubleq1(out_val->sleef_value));
108+
109+
// Compare in longdouble domain
110+
if (std::isnan(in_val->longdouble_value) && std::isnan(roundtrip.longdouble_value))
111+
return 1;
112+
if (in_val->longdouble_value == roundtrip.longdouble_value)
113+
return 1;
114+
if (in_val->longdouble_value == 0.0L && roundtrip.longdouble_value == 0.0L)
115+
return 1;
116+
}
117+
118+
// Values don't match
119+
Sleef_quad sleef_val = quad_to_sleef_quad(in_val, backend_in);
120+
const char *val_str = quad_to_string_adaptive_cstr(&sleef_val, QUAD_STR_WIDTH);
121+
if (val_str != NULL) {
122+
PyErr_Format(PyExc_ValueError,
123+
"QuadPrecision value '%s' cannot be represented exactly in target backend",
124+
val_str);
125+
}
126+
else {
127+
PyErr_SetString(PyExc_ValueError,
128+
"QuadPrecision value cannot be represented exactly in target backend");
129+
}
130+
return -1;
75131
}
76132

77133
template <bool Aligned>
@@ -119,7 +175,8 @@ quad_to_quad_strided_loop(PyArrayMethod_Context *context, char *const data[],
119175
std::memcpy(&out_val.sleef_value, &temp, sizeof(Sleef_quad));
120176
}
121177
}
122-
178+
179+
// check same_value for inter-backend casts
123180
if(same_value_casting)
124181
{
125182
int ret = quad_to_quad_same_value_check(&in_val, backend_in, &out_val, backend_out);
@@ -136,6 +193,7 @@ quad_to_quad_strided_loop(PyArrayMethod_Context *context, char *const data[],
136193
}
137194

138195
// same backend: direct copy
196+
// same_value casting not needed here as values are identical
139197
while(N--) {
140198
quad_value val;
141199
load_quad<Aligned>(in_ptr, backend_in, &val);

quaddtype/numpy_quaddtype/src/scalar_ops.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ quad_richcompare(QuadPrecisionObject *self, PyObject *other, int cmp_op)
134134
Py_INCREF(other);
135135
other_quad = (QuadPrecisionObject *)other;
136136
if (other_quad->backend != backend) {
137+
// we could allow, but this will be bad
138+
// Two values that are different in quad precision,
139+
// might appear equal when converted to double.
137140
PyErr_SetString(PyExc_TypeError,
138141
"Cannot compare QuadPrecision objects with different backends");
139142
Py_DECREF(other_quad);

quaddtype/tests/test_quaddtype.py

Lines changed: 81 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -5577,82 +5577,86 @@ def test_same_value_cast_strings_narrow_width(self, dtype):
55775577
with pytest.raises(ValueError):
55785578
q.astype(dtype, casting="same_value")
55795579

5580-
# @pytest.mark.parametrize("src_backend,dst_backend", [
5581-
# ("sleef", "longdouble"),
5582-
# ("longdouble", "sleef"),
5583-
# ("sleef", "sleef"),
5584-
# ("longdouble", "longdouble")
5585-
# ])
5586-
# def test_quad_to_quad_same_value_casting_passing(self, src_backend, dst_backend):
5587-
# """Test values that should roundtrip exactly between backends."""
5588-
# # Values exactly representable in both backends (and in double, since
5589-
# # inter-backend conversion goes through double)
5590-
# passing_values = [
5591-
# "0.0", "-0.0", "1.0", "-1.0",
5592-
# "0.5", "0.25", "0.125",
5593-
# "2.0", "4.0", "8.0",
5594-
# "inf", "-inf", "nan",
5595-
# "1e100", "-1e-100",
5596-
# str(2**52), # Largest consecutive integer in double
5597-
# ]
5598-
5599-
# for val in passing_values:
5600-
# src = np.array([val], dtype=QuadPrecDType(backend=src_backend))
5601-
# result = src.astype(QuadPrecDType(backend=dst_backend), casting="same_value")
5580+
@pytest.mark.parametrize("src_backend,dst_backend", [
5581+
("sleef", "longdouble"),
5582+
("longdouble", "sleef"),
5583+
("sleef", "sleef"),
5584+
("longdouble", "longdouble")
5585+
])
5586+
def test_quad_to_quad_same_value_casting_passing(self, src_backend, dst_backend):
5587+
"""Test values that should roundtrip exactly between backends."""
5588+
# Values exactly representable in both backends (and in double, since
5589+
# inter-backend conversion goes through double)
5590+
passing_values = [
5591+
0.0, -0.0, 1.0, -1.0,
5592+
0.5, 0.25, 0.125,
5593+
2.0, 4.0, 8.0,
5594+
"inf", "-inf", "nan",
5595+
1e100, -1e-100,
5596+
str(2**52), # Largest consecutive integer in double
5597+
]
5598+
5599+
for val in passing_values:
5600+
src = np.array([val], dtype=QuadPrecDType(backend=src_backend))
5601+
result = src.astype(QuadPrecDType(backend=dst_backend), casting="same_value")
56025602

5603-
# # Verify value is preserved
5604-
# if val == "nan":
5605-
# assert np.isnan(result[0])
5606-
# else:
5607-
# assert result[0] == src[0], f"Value {val} failed for {src_backend} -> {dst_backend}"
5608-
5609-
5610-
# @pytest.mark.parametrize("src_backend,dst_backend", [
5611-
# ("sleef", "longdouble"),
5612-
# ("longdouble", "sleef"),
5613-
# ])
5614-
# def test_quad_to_quad_interbackend_same_value_casting_failing(self, src_backend, dst_backend):
5615-
# """Test values that cannot roundtrip exactly between backends."""
5616-
5617-
# # Inter-backend conversion goes through double, so values outside
5618-
# # double's precision should fail
5619-
# ld_info = np.finfo(np.longdouble)
5620-
5621-
# # Skip if longdouble has same precision as quad (PowerPC binary128)
5622-
# if ld_info.nmant >= 112:
5623-
# pytest.skip("longdouble has same precision as quad on this platform")
5624-
5625-
# # Also need to consider that conversion goes through double
5626-
# # So precision is limited by double (~52 bit mantissa)
5627-
# double_info = np.finfo(np.float64)
5628-
5629-
# # Values that exceed double precision
5630-
# failing_values = [
5631-
# str(2**53 + 1), # First integer not exactly representable in double
5632-
# "1.0000000000000001", # 1 + small epsilon beyond double precision
5633-
# "3.141592653589793238462643383279502884197", # Pi with more than double precision
5634-
# ]
5635-
5636-
# for val in failing_values:
5637-
# src = np.array([val], dtype=QuadPrecDType(backend=src_backend))
5638-
# with pytest.raises(ValueError):
5639-
# src.astype(QuadPrecDType(backend=dst_backend), casting="same_value")
5640-
5641-
5642-
# @pytest.mark.parametrize("backend", ["sleef", "longdouble"])
5643-
# def test_quad_to_quad_same_backend_always_passes(self, backend):
5644-
# """Same backend conversion should always pass same_value."""
5645-
# # Even high-precision values should pass when backend is the same
5646-
# values = [
5647-
# "3.141592653589793238462643383279502884197",
5648-
# "2.718281828459045235360287471352662497757",
5649-
# str(2**113), # Large integer
5650-
# "1e4000", # Large exponent (within quad range)
5651-
# ]
5652-
5653-
# for val in values:
5654-
# src = np.array([val], dtype=QuadPrecDType(backend=backend))
5655-
# result = src.astype(QuadPrecDType(backend=backend), casting="same_value")
5656-
# # Should not raise, and value should be unchanged
5657-
# assert str(result[0]) == str(src[0])
5603+
# Verify value is preserved
5604+
if val == "nan":
5605+
assert np.isnan(result[0])
5606+
else:
5607+
assert float(result[0]) == float(src[0]), f"Value {val} failed for {src_backend} -> {dst_backend}"
5608+
5609+
5610+
@pytest.mark.parametrize("src_backend,dst_backend", [
5611+
("sleef", "longdouble"),
5612+
("longdouble", "sleef"),
5613+
])
5614+
def test_quad_to_quad_interbackend_same_value_casting_failing(self, src_backend, dst_backend):
5615+
"""Test values that cannot roundtrip exactly between backends.
5616+
5617+
Inter-backend conversion goes through double, so values exceeding
5618+
double's precision (~53 bits mantissa) will fail same_value casting.
5619+
"""
5620+
ld_info = np.finfo(np.longdouble)
5621+
double_info = np.finfo(np.float64)
5622+
5623+
# Skip if longdouble has same precision as quad (PowerPC binary128)
5624+
# In that case, sleef <-> longdouble might use a direct path
5625+
if ld_info.nmant >= 112:
5626+
pytest.skip("longdouble has same precision as quad on this platform")
5627+
5628+
# For longdouble -> sleef: only fails if longdouble has more precision than double
5629+
if src_backend == "longdouble" and ld_info.nmant <= double_info.nmant:
5630+
pytest.skip("longdouble has same or less precision than double on this platform")
5631+
5632+
# Values that exceed double precision (53-bit mantissa)
5633+
# These will lose precision when going through the double conversion
5634+
failing_values = [
5635+
str(2**53 + 1), # First integer not exactly representable in double
5636+
"3.141592653589793238462643383279502884197", # Pi with more than double precision
5637+
"1.00000000000000011", # 1 + epsilon beyond double precision
5638+
]
5639+
5640+
for val in failing_values:
5641+
src = np.array([val], dtype=QuadPrecDType(backend=src_backend))
5642+
with pytest.raises(ValueError):
5643+
src.astype(QuadPrecDType(backend=dst_backend), casting="same_value")
5644+
5645+
5646+
@pytest.mark.parametrize("backend", ["sleef", "longdouble"])
5647+
def test_quad_to_quad_same_backend_always_passes(self, backend):
5648+
"""Same backend conversion should always pass same_value."""
5649+
# Even high-precision values should pass when backend is the same
5650+
values = [
5651+
"3.141592653589793238462643383279502884197",
5652+
"2.718281828459045235360287471352662497757",
5653+
str(2**113), # Large integer
5654+
"1e4000", # Large exponent (within quad range)
5655+
]
5656+
5657+
for val in values:
5658+
src = np.array([val], dtype=QuadPrecDType(backend=backend))
5659+
result = src.astype(QuadPrecDType(backend=backend), casting="same_value")
5660+
# Should not raise, and value should be unchanged
5661+
assert str(result[0]) == str(src[0])
56585662

0 commit comments

Comments
 (0)