Skip to content

Commit eae6240

Browse files
authored
Merge pull request #8 from seberg/mpfdtype
ENH: Add `MPFloat.prec` and `MPFDType.prec`, also add basic scalar tests
2 parents 9ce50cf + 483b29a commit eae6240

File tree

8 files changed

+155
-14
lines changed

8 files changed

+155
-14
lines changed

.github/workflows/ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ jobs:
2929
working-directory: metadatadtype
3030
run: |
3131
pytest -vvv --color=yes
32+
- name: install mpfdtype
33+
working-directory: mpfdtype
34+
run: |
35+
sudo apt install libmpfr-dev -y
36+
CFLAGS="-Werror" python -m pip install . --no-build-isolation
37+
- name: Run mpfdtype tests
38+
working-directory: mpfdtype
39+
run: |
40+
pytest -vvv --color=yes
3241
- name: Install unytdtype
3342
working-directory: unytdtype
3443
run: |

mpfdtype/mpfdtype/src/casts.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,9 @@ numpy_to_mpf_strided_loop(PyArrayMethod_Context *context,
182182
T *in = (T *)in_ptr;
183183
mpf_load(out, out_ptr, prec_out);
184184

185-
int res = C_to_mpfr<conv_T, T>(*in, out);
186-
// TODO: At least for ints, could flag out of bounds...
185+
C_to_mpfr<conv_T, T>(*in, out);
186+
// TODO: At least for ints, could flag out of bounds, the return value
187+
// of C_to_mpfr may help with that (it flags imprecisions).
187188
mpf_store(out_ptr, out);
188189

189190
in_ptr += strides[0];

mpfdtype/mpfdtype/src/dtype.c

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ new_MPFDType_instance(mpfr_prec_t precision)
4646
"storage of single float would be too large for precision.");
4747
}
4848
new->base.elsize = sizeof(mpf_storage) + size;
49-
new->base.alignment = _Alignof(mpf_storage);
49+
new->base.alignment = _Alignof(mpf_field);
5050
new->base.flags |= NPY_NEEDS_INIT;
5151

5252
return new;
@@ -213,6 +213,22 @@ MPFDType_repr(MPFDTypeObject *self)
213213
}
214214

215215

216+
PyObject *
217+
MPFDType_get_prec(MPFDTypeObject *self)
218+
{
219+
return PyLong_FromLong(self->precision);
220+
}
221+
222+
223+
NPY_NO_EXPORT PyGetSetDef mpfdtype_getsetlist[] = {
224+
{"prec",
225+
(getter)MPFDType_get_prec,
226+
NULL,
227+
NULL, NULL},
228+
{NULL, NULL, NULL, NULL, NULL}, /* Sentinel */
229+
};
230+
231+
216232
/*
217233
* This is the basic things that you need to create a Python Type/Class in C.
218234
* However, there is a slight difference here because we create a
@@ -226,6 +242,7 @@ PyArray_DTypeMeta MPFDType = {{{
226242
.tp_new = MPFDType_new,
227243
.tp_repr = (reprfunc)MPFDType_repr,
228244
.tp_str = (reprfunc)MPFDType_repr,
245+
.tp_getset = mpfdtype_getsetlist,
229246
}},
230247
/* rest, filled in during DTypeMeta initialization */
231248
};

mpfdtype/mpfdtype/src/dtype.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ typedef struct {
2323
* So not storeing only those (saving 16 bytes of 48 for a 128 bit number)
2424
* removes the need to worry about this.
2525
*/
26-
static_assert(_Alignof(mpfr_t) >= _Alignof(mp_limb_t),
27-
"mpfr_t storage not aligned as much as limb_t?!");
2826
typedef mpfr_t mpf_storage;
2927

3028
extern PyArray_DTypeMeta MPFDType;

mpfdtype/mpfdtype/src/scalar.c

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,22 @@ MPFloat_repr(MPFloatObject* self)
184184
}
185185

186186

187-
int
188-
init_mpf_scalar(void)
187+
static PyObject *
188+
MPFloat_get_prec(MPFloatObject *self)
189189
{
190-
return PyType_Ready(&MPFloat_Type);
190+
return PyLong_FromLong(mpfr_get_prec(self->mpf.x));
191191
}
192192

193193

194+
NPY_NO_EXPORT PyGetSetDef mpfloat_getsetlist[] = {
195+
{"prec",
196+
(getter)MPFloat_get_prec,
197+
NULL,
198+
NULL, NULL},
199+
{NULL, NULL, NULL, NULL, NULL}, /* Sentinel */
200+
};
201+
202+
194203
PyTypeObject MPFloat_Type = {
195204
PyVarObject_HEAD_INIT(NULL, 0)
196205
.tp_name = "MPFDType.MPFDType",
@@ -200,5 +209,13 @@ PyTypeObject MPFloat_Type = {
200209
.tp_repr = (reprfunc)MPFloat_repr,
201210
.tp_str = (reprfunc)MPFloat_str,
202211
.tp_as_number = &mpf_as_number,
203-
.tp_richcompare = &mpf_richcompare,
212+
.tp_richcompare = (richcmpfunc)mpf_richcompare,
213+
.tp_getset = mpfloat_getsetlist,
204214
};
215+
216+
217+
int
218+
init_mpf_scalar(void)
219+
{
220+
return PyType_Ready(&MPFloat_Type);
221+
}

mpfdtype/mpfdtype/src/terrible_hacks.c

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@
2222
* (for larger itemsizes at least).
2323
*/
2424
static void
25-
copyswap_mpf(char *dst, char *src, int swap, PyArrayObject *arr)
25+
copyswap_mpf(char *dst, char *src, int swap, PyArrayObject *ap)
2626
{
27-
PyArray_Descr *descr = PyArray_DESCR(arr);
27+
/* Note that it is probably better to only get the descr from `ap` */
28+
PyArray_Descr *descr = PyArray_DESCR(ap);
2829

2930
memcpy(dst, src, descr->elsize);
3031
}
@@ -33,9 +34,10 @@ copyswap_mpf(char *dst, char *src, int swap, PyArrayObject *arr)
3334
/* Should only be used for sorting, so more complex than necessary, probably */
3435
int compare_mpf(char *in1_ptr, char *in2_ptr, int swap, PyArrayObject *ap)
3536
{
37+
/* Note that it is probably better to only get the descr from `ap` */
3638
mpfr_prec_t precision = ((MPFDTypeObject *)PyArray_DESCR(ap))->precision;
3739

38-
mpfr_t in1, in2;
40+
mpfr_ptr in1, in2;
3941

4042
mpf_load(in1, in1_ptr, precision);
4143
mpf_load(in2, in2_ptr, precision);
@@ -63,8 +65,8 @@ init_terrible_hacks(void) {
6365
return -1;
6466
}
6567
/* ->f slots are the same for all instances (currently). */
66-
descr->base.f->copyswap = &copyswap_mpf;
67-
descr->base.f->compare = &compare_mpf;
68+
descr->base.f->copyswap = (PyArray_CopySwapFunc *)&copyswap_mpf;
69+
descr->base.f->compare = (PyArray_CompareFunc *)&compare_mpf;
6870
Py_DECREF(descr);
6971

7072
return 0;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import os
2+
3+
os.environ["NUMPY_EXPERIMENTAL_DTYPE_API"] = "1"
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import pytest
2+
3+
import sys
4+
import numpy as np
5+
import operator
6+
7+
from mpfdtype import MPFDType, MPFloat
8+
9+
10+
def test_create_scalar_simple():
11+
# currently inferring 53bit precision from float:
12+
assert MPFloat(12.).prec == 53
13+
# currently infers 64bit or 32bit depending on system:
14+
assert MPFloat(1).prec == sys.maxsize.bit_count() + 1
15+
16+
assert MPFloat(MPFloat(12.)).prec == 53
17+
assert MPFloat(MPFloat(1)).prec == sys.maxsize.bit_count() + 1
18+
19+
20+
def test_create_scalar_prec():
21+
assert MPFloat(1, prec=100).prec == 100
22+
assert MPFloat(12., prec=123).prec == 123
23+
assert MPFloat("12.234", prec=1000).prec == 1000
24+
25+
mpf1 = MPFloat("12.4325", prec=120)
26+
mpf2 = MPFloat(mpf1, prec=150)
27+
assert mpf1 == mpf2
28+
assert mpf2.prec == 150
29+
30+
31+
def test_basic_equality():
32+
assert MPFloat(12) == MPFloat(12.) == MPFloat("12.00", prec=10)
33+
34+
35+
@pytest.mark.parametrize("val", [123532.543, 12893283.5])
36+
def test_scalar_repr(val):
37+
# For non exponentials at least, the repr matches:
38+
val_repr = f"{val:e}".upper()
39+
expected = f"MPFloat('{val_repr}', prec=20)"
40+
assert repr(MPFloat(val, prec=20)) == expected
41+
42+
@pytest.mark.parametrize("op",
43+
["add", "sub", "mul", "pow"])
44+
@pytest.mark.parametrize("other", [3., 12.5, 100., np.nan, np.inf])
45+
def test_binary_ops(op, other):
46+
# Generally, the math ops should behave the same as double math if they
47+
# use double precision (which they currently do).
48+
# (double could have errors, but not for these simple ops)
49+
op = getattr(operator, op)
50+
try:
51+
expected = op(12.5, other)
52+
except Exception as e:
53+
with pytest.raises(type(e)):
54+
op(MPFloat(12.5), other)
55+
with pytest.raises(type(e)):
56+
op(12.5, MPFloat(other))
57+
with pytest.raises(type(e)):
58+
op(MPFloat(12.5), MPFloat(other))
59+
else:
60+
if np.isnan(expected):
61+
# Avoiding isnan (which was also not implemented when written)
62+
res = op(MPFloat(12.5), other)
63+
assert res != res
64+
res = op(12.5, MPFloat(other))
65+
assert res != res
66+
res = op(MPFloat(12.5), MPFloat(other))
67+
assert res != res
68+
else:
69+
assert op(MPFloat(12.5), other) == expected
70+
assert op(12.5, MPFloat(other)) == expected
71+
assert op(MPFloat(12.5), MPFloat(other)) == expected
72+
73+
74+
@pytest.mark.parametrize("op",
75+
["eq", "ne", "le", "lt", "ge", "gt"])
76+
@pytest.mark.parametrize("other", [3., 12.5, 100., np.nan, np.inf])
77+
def test_comparisons(op, other):
78+
op = getattr(operator, op)
79+
expected = op(12.5, other)
80+
assert op(MPFloat(12.5), other) is expected
81+
assert op(12.5, MPFloat(other)) is expected
82+
assert op(MPFloat(12.5), MPFloat(other)) is expected
83+
84+
85+
@pytest.mark.parametrize("op",
86+
["neg", "pos", "abs"])
87+
@pytest.mark.parametrize("val", [3., 12.5, 100., np.nan, np.inf])
88+
def test_comparisons(op, val):
89+
op = getattr(operator, op)
90+
expected = op(val)
91+
if np.isnan(expected):
92+
assert op(MPFloat(val)) != op(MPFloat(val))
93+
else:
94+
assert op(MPFloat(val)) == expected

0 commit comments

Comments
 (0)