Skip to content

Commit e46eec9

Browse files
authored
Merge pull request #192 from SwayamInSync/modf
2 parents 59cf5e5 + 00fd67e commit e46eec9

File tree

4 files changed

+240
-2
lines changed

4 files changed

+240
-2
lines changed

quaddtype/numpy_quaddtype/src/ops.hpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
// Unary Quad Operations
1212
typedef Sleef_quad (*unary_op_quad_def)(const Sleef_quad *);
13+
// Unary Quad operations with 2 outputs (for modf, frexp)
14+
typedef void (*unary_op_2out_quad_def)(const Sleef_quad *, Sleef_quad *, Sleef_quad *);
1315

1416
static Sleef_quad
1517
quad_negative(const Sleef_quad *op)
@@ -1361,6 +1363,23 @@ ld_spacing(const long double *x)
13611363
return result;
13621364
}
13631365

1366+
// Unary operations with 2 outputs
1367+
static inline void
1368+
quad_modf(const Sleef_quad *a, Sleef_quad *out_fractional, Sleef_quad *out_integral)
1369+
{
1370+
// int part stored in out_integral
1371+
*out_fractional = Sleef_modfq1(*a, out_integral);
1372+
}
1373+
1374+
// Unary long double operations with 2 outputs
1375+
typedef void (*unary_op_2out_longdouble_def)(const long double *, long double *, long double *);
1376+
1377+
static inline void
1378+
ld_modf(const long double *a, long double *out_fractional, long double *out_integral)
1379+
{
1380+
*out_fractional = modfl(*a, out_integral);
1381+
}
1382+
13641383
// comparison quad functions
13651384
typedef npy_bool (*cmp_quad_def)(const Sleef_quad *, const Sleef_quad *);
13661385

quaddtype/numpy_quaddtype/src/umath/unary_ops.cpp

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,147 @@ create_quad_logical_not_ufunc(PyObject *numpy, const char *ufunc_name)
267267
return 0;
268268
}
269269

270+
// Resolver for unary ufuncs with 2 outputs (like modf)
271+
static NPY_CASTING
272+
quad_unary_op_2out_resolve_descriptors(PyObject *self, PyArray_DTypeMeta *const dtypes[],
273+
PyArray_Descr *const given_descrs[], PyArray_Descr *loop_descrs[],
274+
npy_intp *NPY_UNUSED(view_offset))
275+
{
276+
// Input descriptor
277+
Py_INCREF(given_descrs[0]);
278+
loop_descrs[0] = given_descrs[0];
279+
280+
// Output descriptors (2 outputs)
281+
for (int i = 1; i < 3; i++) {
282+
if (given_descrs[i] == NULL) {
283+
Py_INCREF(given_descrs[0]);
284+
loop_descrs[i] = given_descrs[0];
285+
}
286+
else {
287+
Py_INCREF(given_descrs[i]);
288+
loop_descrs[i] = given_descrs[i];
289+
}
290+
}
291+
292+
QuadPrecDTypeObject *descr_in = (QuadPrecDTypeObject *)given_descrs[0];
293+
QuadPrecDTypeObject *descr_out1 = (QuadPrecDTypeObject *)loop_descrs[1];
294+
QuadPrecDTypeObject *descr_out2 = (QuadPrecDTypeObject *)loop_descrs[2];
295+
296+
if (descr_in->backend != descr_out1->backend || descr_in->backend != descr_out2->backend) {
297+
return NPY_UNSAFE_CASTING;
298+
}
299+
300+
return NPY_NO_CASTING;
301+
}
302+
303+
// Strided loop for unary ops with 2 outputs (unaligned)
304+
template <unary_op_2out_quad_def sleef_op, unary_op_2out_longdouble_def longdouble_op>
305+
int
306+
quad_generic_unary_op_2out_strided_loop_unaligned(PyArrayMethod_Context *context, char *const data[],
307+
npy_intp const dimensions[], npy_intp const strides[],
308+
NpyAuxData *auxdata)
309+
{
310+
npy_intp N = dimensions[0];
311+
char *in_ptr = data[0];
312+
char *out1_ptr = data[1];
313+
char *out2_ptr = data[2];
314+
npy_intp in_stride = strides[0];
315+
npy_intp out1_stride = strides[1];
316+
npy_intp out2_stride = strides[2];
317+
318+
QuadPrecDTypeObject *descr = (QuadPrecDTypeObject *)context->descriptors[0];
319+
QuadBackendType backend = descr->backend;
320+
size_t elem_size = (backend == BACKEND_SLEEF) ? sizeof(Sleef_quad) : sizeof(long double);
321+
322+
quad_value in, out1, out2;
323+
while (N--) {
324+
memcpy(&in, in_ptr, elem_size);
325+
if (backend == BACKEND_SLEEF) {
326+
sleef_op(&in.sleef_value, &out1.sleef_value, &out2.sleef_value);
327+
}
328+
else {
329+
longdouble_op(&in.longdouble_value, &out1.longdouble_value, &out2.longdouble_value);
330+
}
331+
memcpy(out1_ptr, &out1, elem_size);
332+
memcpy(out2_ptr, &out2, elem_size);
333+
334+
in_ptr += in_stride;
335+
out1_ptr += out1_stride;
336+
out2_ptr += out2_stride;
337+
}
338+
return 0;
339+
}
340+
341+
// Strided loop for unary ops with 2 outputs (aligned)
342+
template <unary_op_2out_quad_def sleef_op, unary_op_2out_longdouble_def longdouble_op>
343+
int
344+
quad_generic_unary_op_2out_strided_loop_aligned(PyArrayMethod_Context *context, char *const data[],
345+
npy_intp const dimensions[], npy_intp const strides[],
346+
NpyAuxData *auxdata)
347+
{
348+
npy_intp N = dimensions[0];
349+
char *in_ptr = data[0];
350+
char *out1_ptr = data[1];
351+
char *out2_ptr = data[2];
352+
npy_intp in_stride = strides[0];
353+
npy_intp out1_stride = strides[1];
354+
npy_intp out2_stride = strides[2];
355+
356+
QuadPrecDTypeObject *descr = (QuadPrecDTypeObject *)context->descriptors[0];
357+
QuadBackendType backend = descr->backend;
358+
359+
while (N--) {
360+
if (backend == BACKEND_SLEEF) {
361+
sleef_op((Sleef_quad *)in_ptr, (Sleef_quad *)out1_ptr, (Sleef_quad *)out2_ptr);
362+
}
363+
else {
364+
longdouble_op((long double *)in_ptr, (long double *)out1_ptr, (long double *)out2_ptr);
365+
}
366+
in_ptr += in_stride;
367+
out1_ptr += out1_stride;
368+
out2_ptr += out2_stride;
369+
}
370+
return 0;
371+
}
372+
373+
// Create unary ufunc with 2 outputs
374+
template <unary_op_2out_quad_def sleef_op, unary_op_2out_longdouble_def longdouble_op>
375+
int
376+
create_quad_unary_2out_ufunc(PyObject *numpy, const char *ufunc_name)
377+
{
378+
PyObject *ufunc = PyObject_GetAttrString(numpy, ufunc_name);
379+
if (ufunc == NULL) {
380+
return -1;
381+
}
382+
383+
// 1 input, 2 outputs
384+
PyArray_DTypeMeta *dtypes[3] = {&QuadPrecDType, &QuadPrecDType, &QuadPrecDType};
385+
386+
PyType_Slot slots[] = {
387+
{NPY_METH_resolve_descriptors, (void *)&quad_unary_op_2out_resolve_descriptors},
388+
{NPY_METH_strided_loop,
389+
(void *)&quad_generic_unary_op_2out_strided_loop_aligned<sleef_op, longdouble_op>},
390+
{NPY_METH_unaligned_strided_loop,
391+
(void *)&quad_generic_unary_op_2out_strided_loop_unaligned<sleef_op, longdouble_op>},
392+
{0, NULL}};
393+
394+
PyArrayMethod_Spec Spec = {
395+
.name = "quad_unary_2out",
396+
.nin = 1,
397+
.nout = 2,
398+
.casting = NPY_NO_CASTING,
399+
.flags = NPY_METH_SUPPORTS_UNALIGNED,
400+
.dtypes = dtypes,
401+
.slots = slots,
402+
};
403+
404+
if (PyUFunc_AddLoopFromSpec(ufunc, &Spec) < 0) {
405+
return -1;
406+
}
407+
408+
return 0;
409+
}
410+
270411
int
271412
init_quad_unary_ops(PyObject *numpy)
272413
{
@@ -394,5 +535,10 @@ init_quad_unary_ops(PyObject *numpy)
394535
return -1;
395536
}
396537

538+
// Unary operations with 2 outputs
539+
if (create_quad_unary_2out_ufunc<quad_modf, ld_modf>(numpy, "modf") < 0) {
540+
return -1;
541+
}
542+
397543
return 0;
398544
}

quaddtype/release_tracker.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
| copysign |||
8080
| nextafter |||
8181
| spacing |||
82-
| modf | | |
82+
| modf | | |
8383
| ldexp | | |
8484
| frexp | | |
8585
| floor |||

quaddtype/tests/test_quaddtype.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2429,7 +2429,7 @@ def test_subnormal_range(self, x):
24292429
if result != q_zero:
24302430
assert np.signbit(result) == np.signbit(q_x), \
24312431
f"spacing({x}) should have same sign as {x}"
2432-
2432+
24332433
def test_smallest_normal_spacing(self):
24342434
"""Test spacing for smallest normal value and 2*smallest normal"""
24352435
finfo = np.finfo(QuadPrecDType())
@@ -2457,3 +2457,76 @@ def test_smallest_normal_spacing(self):
24572457
f"spacing(2*smallest_normal) should be {expected2}, got {result2}"
24582458
assert result2 > q_zero, "Result should be positive"
24592459

2460+
2461+
class TestModf:
2462+
"""Test cases for np.modf function with QuadPrecision dtype"""
2463+
2464+
@pytest.mark.parametrize("x", [
2465+
# Basic positive/negative numbers
2466+
"3.14159", "-3.14159", "2.71828", "-2.71828", "1.5", "-1.5", "0.75", "-0.75",
2467+
# Integers (fractional part should be zero)
2468+
"0.0", "-0.0", "1.0", "-1.0", "5.0", "-5.0", "42.0", "-42.0",
2469+
# Small numbers
2470+
"0.001", "-0.001", "0.000123", "-0.000123",
2471+
# Large numbers
2472+
"1e10", "-1e10", "1e15", "-1e15",
2473+
# Numbers close to integers
2474+
"0.999999999999", "-0.999999999999", "1.000000000001", "-1.000000000001",
2475+
# Special values
2476+
"inf", "-inf", "nan",
2477+
# Edge cases for sign consistency
2478+
"5.7", "-5.7", "0.3", "-0.3"
2479+
])
2480+
def test_modf(self, x):
2481+
"""Test modf against NumPy's behavior"""
2482+
quad_x = QuadPrecision(x)
2483+
2484+
# Compute modf for both QuadPrecision and float64
2485+
quad_frac, quad_int = np.modf(quad_x)
2486+
2487+
# Create numpy float64 for reference
2488+
try:
2489+
float_x = np.float64(x)
2490+
np_frac, np_int = np.modf(float_x)
2491+
except (ValueError, OverflowError):
2492+
# Handle cases where string can't convert to float64 (like "nan")
2493+
float_x = np.float64(float(x))
2494+
np_frac, np_int = np.modf(float_x)
2495+
2496+
# Check return types
2497+
assert isinstance(quad_frac, QuadPrecision), f"Fractional part should be QuadPrecision for {x}"
2498+
assert isinstance(quad_int, QuadPrecision), f"Integral part should be QuadPrecision for {x}"
2499+
2500+
# Direct comparison with NumPy results
2501+
if np.isnan(np_frac):
2502+
assert np.isnan(quad_frac), f"Expected NaN fractional part for modf({x})"
2503+
else:
2504+
np.testing.assert_allclose(
2505+
quad_frac, np_frac, rtol=1e-12, atol=1e-15,
2506+
err_msg=f"Fractional part mismatch for modf({x})"
2507+
)
2508+
2509+
if np.isnan(np_int):
2510+
assert np.isnan(quad_int), f"Expected NaN integral part for modf({x})"
2511+
elif np.isinf(np_int):
2512+
assert np.isinf(quad_int), f"Expected inf integral part for modf({x})"
2513+
assert np.signbit(quad_int) == np.signbit(np_int), f"Sign mismatch for inf integral part modf({x})"
2514+
else:
2515+
np.testing.assert_allclose(
2516+
quad_int, np_int, rtol=1e-12, atol=0,
2517+
err_msg=f"Integral part mismatch for modf({x})"
2518+
)
2519+
2520+
# Check sign preservation for zero values
2521+
if np_frac == 0.0:
2522+
assert np.signbit(quad_frac) == np.signbit(np_frac), f"Zero fractional sign mismatch for modf({x})"
2523+
if np_int == 0.0:
2524+
assert np.signbit(quad_int) == np.signbit(np_int), f"Zero integral sign mismatch for modf({x})"
2525+
2526+
# Verify reconstruction property for finite values
2527+
if np.isfinite(float_x) and not np.isnan(float_x):
2528+
reconstructed = quad_int + quad_frac
2529+
np.testing.assert_allclose(
2530+
reconstructed, quad_x, rtol=1e-12, atol=1e-15,
2531+
err_msg=f"Reconstruction failed for modf({x}): {quad_int} + {quad_frac} != {quad_x}"
2532+
)

0 commit comments

Comments
 (0)