Skip to content

Commit 5c5d791

Browse files
committed
adding quad->str same_vale
1 parent 30f6b95 commit 5c5d791

2 files changed

Lines changed: 144 additions & 31 deletions

File tree

quaddtype/numpy_quaddtype/src/casts.cpp

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,74 @@ quad_to_string_adaptive_cstr(Sleef_quad *sleef_val, npy_intp unicode_size_chars)
365365

366366
}
367367

368+
/**
369+
* Same-value check for quad to string conversions.
370+
* Works for bytes (S), unicode (U), and StringDType.
371+
*
372+
* Performs a roundtrip: quad -> string -> quad and verifies the value is preserved.
373+
* The check uses the truncated string (up to str_len chars) to ensure the output
374+
* buffer can faithfully represent the original value.
375+
*
376+
* @param in_val The input quad value
377+
* @param str_buf The string representation (may be longer than str_len)
378+
* @param str_len Length of the string that will actually be stored (excluding null terminator)
379+
* @param backend The quad backend type
380+
* @return 1 if same_value check passes, -1 if it fails (sets Python error)
381+
*/
382+
static inline int
383+
quad_to_string_same_value_check(quad_value in_val, const char *str_buf, npy_intp str_len,
384+
QuadBackendType backend)
385+
{
386+
char *truncated_str = (char *)malloc(str_len + 1);
387+
if (truncated_str == NULL) {
388+
PyErr_NoMemory();
389+
return -1;
390+
}
391+
memcpy(truncated_str, str_buf, str_len);
392+
truncated_str[str_len] = '\0';
393+
394+
// Parse the truncated string back to quad
395+
quad_value roundtrip;
396+
char *endptr;
397+
398+
int err = NumPyOS_ascii_strtoq(truncated_str, backend, &roundtrip, &endptr);
399+
if (err < 0) {
400+
PyErr_Format(PyExc_ValueError,
401+
"QuadPrecision value cannot be represented exactly: string '%s' failed to parse back",
402+
truncated_str);
403+
free(truncated_str);
404+
return -1;
405+
}
406+
free(truncated_str);
407+
408+
// Compare original and roundtripped values
409+
if (backend == BACKEND_SLEEF) {
410+
// NaN == NaN for same_value purposes
411+
if (Sleef_iunordq1(in_val.sleef_value, roundtrip.sleef_value))
412+
return 1;
413+
if (Sleef_icmpeqq1(in_val.sleef_value, roundtrip.sleef_value))
414+
return 1;
415+
// Handle -0.0 == +0.0 case
416+
if (Sleef_icmpeqq1(in_val.sleef_value, QUAD_ZERO) &&
417+
Sleef_icmpeqq1(roundtrip.sleef_value, QUAD_ZERO))
418+
return 1;
419+
}
420+
else {
421+
if (std::isnan(in_val.longdouble_value) && std::isnan(roundtrip.longdouble_value))
422+
return 1;
423+
if (in_val.longdouble_value == roundtrip.longdouble_value)
424+
return 1;
425+
// Handle -0.0 == +0.0 case
426+
if (in_val.longdouble_value == 0.0L && roundtrip.longdouble_value == 0.0L)
427+
return 1;
428+
}
429+
430+
PyErr_Format(PyExc_ValueError,
431+
"QuadPrecision value cannot be represented exactly in target string dtype "
432+
"(string width too narrow or precision loss occurred)");
433+
return -1;
434+
}
435+
368436
template <bool Aligned>
369437
static int
370438
quad_to_unicode_loop(PyArrayMethod_Context *context, char *const data[],
@@ -382,37 +450,37 @@ quad_to_unicode_loop(PyArrayMethod_Context *context, char *const data[],
382450
QuadBackendType backend = descr_in->backend;
383451

384452
npy_intp unicode_size_chars = descrs[1]->elsize / 4;
453+
int same_value_casting = ((context->flags & NPY_SAME_VALUE_CONTEXT_FLAG) == NPY_SAME_VALUE_CONTEXT_FLAG);
385454

386455
while (N--) {
387456
quad_value in_val = load_quad<Aligned>(in_ptr, backend);
388457

389458
// Convert to Sleef_quad for Dragon4
390459
Sleef_quad sleef_val = quad_to_sleef_quad(&in_val, backend);
391460

392-
// Get string representation with adaptive notation
393-
PyObject *py_str = quad_to_string_adaptive(&sleef_val, unicode_size_chars);
394-
if (py_str == NULL) {
461+
const char *temp_str = quad_to_string_adaptive_cstr(&sleef_val, unicode_size_chars);
462+
if (temp_str == NULL) {
395463
return -1;
396464
}
397465

398-
const char *temp_str = PyUnicode_AsUTF8(py_str);
399-
if (temp_str == NULL) {
400-
Py_DECREF(py_str);
401-
return -1;
466+
npy_intp str_len = strnlen(temp_str, unicode_size_chars);
467+
468+
// Perform same_value check if requested
469+
if (same_value_casting) {
470+
if (quad_to_string_same_value_check(in_val, temp_str, str_len, backend) < 0) {
471+
return -1;
472+
}
402473
}
403474

404475
// Convert char string to UCS4 and store in output
405476
Py_UCS4 *out_ucs4 = (Py_UCS4 *)out_ptr;
406-
npy_intp str_len = strnlen(temp_str, unicode_size_chars);
407477
for (npy_intp i = 0; i < str_len; i++) {
408478
out_ucs4[i] = (Py_UCS4)temp_str[i];
409479
}
410480
for (npy_intp i = str_len; i < unicode_size_chars; i++) {
411481
out_ucs4[i] = 0;
412482
}
413483

414-
Py_DECREF(py_str);
415-
416484
in_ptr += in_stride;
417485
out_ptr += out_stride;
418486
}
@@ -575,25 +643,29 @@ quad_to_bytes_loop(PyArrayMethod_Context *context, char *const data[],
575643
QuadBackendType backend = descr_in->backend;
576644

577645
npy_intp bytes_size = descrs[1]->elsize;
646+
int same_value_casting = ((context->flags & NPY_SAME_VALUE_CONTEXT_FLAG) == NPY_SAME_VALUE_CONTEXT_FLAG);
578647

579648
while (N--) {
580649
quad_value in_val = load_quad<Aligned>(in_ptr, backend);
581650
Sleef_quad sleef_val = quad_to_sleef_quad(&in_val, backend);
582-
PyObject *py_str = quad_to_string_adaptive(&sleef_val, bytes_size);
583-
if (py_str == NULL) {
584-
return -1;
585-
}
586-
const char *temp_str = PyUnicode_AsUTF8(py_str);
651+
652+
const char *temp_str = quad_to_string_adaptive_cstr(&sleef_val, bytes_size);
587653
if (temp_str == NULL) {
588-
Py_DECREF(py_str);
589654
return -1;
590655
}
591656

657+
npy_intp str_len = strnlen(temp_str, bytes_size);
658+
659+
// Perform same_value check if requested
660+
if (same_value_casting) {
661+
if (quad_to_string_same_value_check(in_val, temp_str, str_len, backend) < 0) {
662+
return -1;
663+
}
664+
}
665+
592666
// Copy string to output buffer, padding with nulls
593667
strncpy(out_ptr, temp_str, bytes_size);
594668

595-
Py_DECREF(py_str);
596-
597669
in_ptr += in_stride;
598670
out_ptr += out_stride;
599671
}
@@ -726,6 +798,7 @@ quad_to_stringdtype_strided_loop(PyArrayMethod_Context *context, char *const dat
726798
QuadPrecDTypeObject *descr_in = (QuadPrecDTypeObject *)descrs[0];
727799
PyArray_StringDTypeObject *str_descr = (PyArray_StringDTypeObject *)descrs[1];
728800
QuadBackendType backend = descr_in->backend;
801+
int same_value_casting = ((context->flags & NPY_SAME_VALUE_CONTEXT_FLAG) == NPY_SAME_VALUE_CONTEXT_FLAG);
729802

730803
npy_string_allocator *allocator = NpyString_acquire_allocator(str_descr);
731804

@@ -743,6 +816,14 @@ quad_to_stringdtype_strided_loop(PyArrayMethod_Context *context, char *const dat
743816

744817
Py_ssize_t str_size = strnlen(str_buf, QUAD_STR_WIDTH);
745818

819+
// Perform same_value check if requested
820+
if (same_value_casting) {
821+
if (quad_to_string_same_value_check(in_val, str_buf, str_size, backend) < 0) {
822+
NpyString_release_allocator(allocator);
823+
return -1;
824+
}
825+
}
826+
746827
npy_packed_static_string *out_ps = (npy_packed_static_string *)out_ptr;
747828
if (NpyString_pack(allocator, out_ps, str_buf, (size_t)str_size) < 0) {
748829
NpyString_release_allocator(allocator);

quaddtype/tests/test_quaddtype.py

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5529,16 +5529,48 @@ def test_same_value_cast_floats_precision_loss(self, dtype):
55295529
with pytest.raises(ValueError):
55305530
q.astype(dtype, casting="same_value")
55315531

5532-
# @pytest.mark.parametrize("dtype", [
5533-
# "S50", "U50", "<U50", ">U50", "S100", "U100", "<U100", ">U100", np.dtypes.StringDType()])
5534-
# @pytest.mark.parametrize("values", [
5535-
# ])
5536-
# def test_same_value_cast_strings_enough_width(self, dtype, values):
5537-
# pass
5538-
5539-
# @pytest.mark.parametrize("dtype", [
5540-
# "S20", "U20", "<U20", ">U20"])
5541-
# @pytest.mark.parametrize("values", [
5542-
# ])
5543-
# def test_same_value_cast_strings_small_width(self, dtype, values):
5544-
# pass
5532+
@pytest.mark.parametrize("dtype", [
5533+
"S50", "U50", "<U50", "S100", "U100", np.dtypes.StringDType()
5534+
])
5535+
def test_same_value_cast_strings_enough_width(self, dtype):
5536+
"""Test that string types with enough width can represent quad values exactly."""
5537+
values = [
5538+
"0.0", "-0.0", "1.0", "-1.0",
5539+
"3.14159265358979323846264338327950288", # pi with full quad precision
5540+
"inf", "-inf", "nan",
5541+
"1.23e100", "-4.56e-100",
5542+
]
5543+
5544+
for val in values:
5545+
q = np.array([val], dtype=QuadPrecDType())
5546+
result = q.astype(dtype, casting="same_value")
5547+
# Convert back and verify
5548+
back = result.astype(QuadPrecDType())
5549+
if np.isnan(q[0]):
5550+
assert np.isnan(back[0]), f"NaN roundtrip failed for {dtype}"
5551+
else:
5552+
assert q[0] == back[0], f"Value {val} roundtrip failed for {dtype}"
5553+
5554+
@pytest.mark.parametrize("dtype", ["S10", "U10", "<U10"])
5555+
def test_same_value_cast_strings_narrow_width(self, dtype):
5556+
"""Test that string types with narrow width fail for values that need more precision."""
5557+
# Values that can fit in 10 chars should pass
5558+
passing_values = ["0.0", "1.0", "-1.0", "inf", "-inf", "nan"]
5559+
for val in passing_values:
5560+
q = np.array([val], dtype=QuadPrecDType())
5561+
result = q.astype(dtype, casting="same_value")
5562+
back = result.astype(QuadPrecDType())
5563+
if np.isnan(q[0]):
5564+
assert np.isnan(back[0])
5565+
else:
5566+
assert q[0] == back[0], f"Value {val} should roundtrip in {dtype}"
5567+
5568+
# Values that need more than 10 chars should fail
5569+
failing_values = [
5570+
"3.14159265358979323846264338327950288", # pi
5571+
"1.23456789012345", # needs > 10 chars
5572+
]
5573+
for val in failing_values:
5574+
q = np.array([val], dtype=QuadPrecDType())
5575+
with pytest.raises(ValueError):
5576+
q.astype(dtype, casting="same_value")

0 commit comments

Comments
 (0)