-
-
Notifications
You must be signed in to change notification settings - Fork 10.9k
ENH: Use Dragon4 algorithm to print floating values #9941
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Output of the code in my last comment, with this PR:
Output with master:
|
dd3b272
to
a96bc04
Compare
Out of curiosity - what keeps us from directly using the C++ version? |
The spaces in
look pretty bad to me, but I understand the rationale. I'd perhaps argue for zero padding to make them all the same length. Otherwise it causes confusion for users comparing the last digits. What does this give?
|
Re: the spacing in the float32 array: That is behavior is still to be decided, now that we have more choices due to the existence of a dragon4 interface. Note that example only gets output for Also, here is what this PR currently outputs by default for your code: [1]: a = 10.**np.arange(20)
[2]: a[0] = 123456
[3]: a
array([1.23456e+05, 1.00000e+01, 1.00000e+02, 1.00000e+03, 1.00000e+04,
1.00000e+05, 1.00000e+06, 1.00000e+07, 1.00000e+08, 1.00000e+09,
1.00000e+10, 1.00000e+11, 1.00000e+12, 1.00000e+13, 1.00000e+14,
1.00000e+15, 1.00000e+16, 1.00000e+17, 1.00000e+18, 1.00000e+19]) |
Also, another comment on the spacing in We already get funny spacing like that in current numpy master: [31]: array([0.32254803, 0.8584671, 0.07836795, 0.2622], dtype=float32)
array([ 0.32254803, 0.8584671 , 0.07836795, 0.2622 ], dtype=float32) (in fact you can also see this in the "master" output I pasted above) |
As for leaving it in C++, I honestly didn't consider that carefully since I assumed it would be harder/more confusing to try to interface to it than to just port to C. Also, while I didn't modify the dragon4 code itself much, I did pretty heavily rewrite the "wrapper" part of the code that converts dragon4 output to a string. |
More thoughts on spacing: I don't think padding with 0s is correct, since the "unnecessary" digits which were dropped are not necessarily 0. To avoid the trailing spaces, probably we could do as follows: In the first pass in FloatingFormat, figure out the maximum fractional length of the data, when output with all the trimming. Then, set the precision to that value in the second pass, and don't do any trimming in the second pass (trim-mode 'k'), thus keeping the extra digits in the values that were a bit shorter in the first pass. |
ef7cf07
to
b133177
Compare
642677b
to
bc5141c
Compare
The test failures are due to ieee extended precision on 32 linux being aligned on 4 byte boundaries, hence |
Yes, I thought I accounted for that with the |
Also I figured out the Also, in principle it is possible to change the rounding mode, see GNU libc's discussion (link). However, I think CPython has forced/assumed the rounding mode to be IEEE unbiased, so I think it is safe to assume the same for numpy. |
numpy/core/src/multiarray/dragon4.h
Outdated
|
||
typedef enum TrimMode | ||
{ | ||
TrimMode_None, // don't trim zeros, always leave a decimal point |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should convert these to C style comments.
Missing include statement. Need |
Assumed, rather than forced. The dtoa.c code is a bit of a mix: some parts (mainly the integer arithmetic parts) explicitly construct the round-ties-to-even results, without help from the FPU. Others (the fast paths that use floating-point), assume that the FPU control word remains at its default of round-ties-to-even. If the FPU rounding mode were set to something else, it's probable that Python's So yes, while in principle assuming that the x87/SSE2 rounding mode settings will be the right ones makes me a bit nervous, in practice it appears to be a fairly safe assumption. Also, the dtoa.c code is written in such a way that infinite loops could happen (at one point I started refactoring to eliminate the potential for infinite loops, but that project never got completed); I'd hope that the Dragon4 code is a bit better behaved in that respect. [Full disclosure: I was responsible, with Eric Smith, for implementing the CPython shortest repr implementation, though of course the original core |
@mdickinson thanks for the links to the issues. Assuming IEEE unbiased rounding in this PR is "safe" in the sense that it won't crash. I think the worst case scenario is that we spit out floating point strings that don't covert back to their original values, eg in the case of 1e23. (By the way, I am discussing the rounding mode issues with Ryan Juckett over on his website, linked above). I don't really want to go through the effort of accounting for all the rounding modes here. But at least maybe I could add a check to |
0901003
to
eae2a80
Compare
Updated
When I come back to this I'm going to work on an arrayprint.py interface and on choosing the right defaults. I'm going to try adding a
|
I think you need a separate |
The rounding still worries me and might be a problem. I suppose we need to rely on the tests to check if that is a problem. Also, out of curiosity, what were the python (hence compiler) versions that had trouble with 1e23? |
@@ -463,6 +483,23 @@ def array2string(a, max_line_width=None, precision=None, | ||
(whitespace character) in the sign position of positive values. If | ||
'-', omit the sign character of positive values. If 'legacy', print a | ||
space for positive values except in 0d arrays. | ||
floatmode : str, optional |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we sure we don't want float_mode
for consistency with suppress_small
?
return format_longfloat(x, self.precision) | ||
|
||
|
||
class LongComplexFormat(object): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we might need to keep this for compatibility, as a subclass that does nothing but throw a warning in it's constructor.
@@ -597,8 +634,8 @@ def _formatArray(a, format_function, rank, max_line_len, | ||
summary_insert).rstrip()+']\n' | ||
return s | ||
|
||
class FloatFormat(object): | ||
def __init__(self, data, precision, suppress_small, sign=False): | ||
class FloatingFormat(object): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment about leaving a public FloatFormat
object visible, assuming we're considering this public API.
Docstring saying For np.floating objects
or something would be nice too
# XXX Have to add something to determine the width to use a la FloatFormat | ||
# Right now, things won't line up properly | ||
def __init__(self, precision, sign=False): | ||
class ComplexFormat(object): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe ComplexFloatingFormat
to match np.complexfloating
? (#9962)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really pleased to see all the arrayprint formatters merge together, but a couple nits about names and compatibility in arrayprint
, which we should probably look at before 1.14
*/ | ||
|
||
#define PREC @NAME@PREC_@KIND@ | ||
static PyObject * | ||
@name@type_@kind@_either(npy_@name@ val, TrimMode trim_pos, TrimMode trim_sci, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could do with a comment explaining what this function is for - "either" isn't super clear
static int | ||
c@name@type_print(PyObject *v, FILE *fp, int flags) | ||
static PyObject * | ||
halftype_@kind@(PyObject *self) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this special compared to the other ones? Why?
elif issubclass(dtypeobj, _nt.complexfloating): | ||
if issubclass(dtypeobj, _nt.clongfloat): | ||
return formatdict['longcomplexfloat']() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This means that users setting a longcomplexfloat
formatter will now see it being ignored silently
Deleted my comments about whitespace, and just added a STY commit to #9962, to reduce noise here |
This is a C port of Ryan Juckett's C++ implementation (link) of the Dragon4 algorithm for printing floating-point values to numpy. See earlier discussion in #9919 and #9932, which are both replaced by this PR. Please check the license at the top of the dragon4.c file to confirm it is suitable.
This is a WIP and I am still deciding some of the behavior, but it is in "working" state. I am putting it up now to run tests, and if anyone wants to try it out. Here's a quick summary:
np.core.multiarray.dragon4_positional
andnp.core.multiarray.dragon4_scientific
. Documentation of arguments is in the C code, but you can call it with a single python floating scalar argument to try it out.str
orformatter
. #9332.arrayprint.py
code to use dragon4. I also added a new option, that you can setnp.printoptions(precision='unique')
, which will print the minimum number of digits to uniquely specify the value. I left the default atprecision=8
for now.arrayprint.py
I've also stolen/reworked theFormatFloat
generalizations from WIP: MAINT: Rewrite LongFloatFormat, trim zeros in scientific notation output #9919. Now all floating types just use a singleFloatingFormat
formatter, which calls the dragon4 routines.Some examples that you can compare to the old behavior:
I still need to do some cleanups and tests. Also, there may be a bug in the original implementation because
np.float64(1e23)
isn't nicely rounded, unlike the other powers of 10.