Repository navigation
High accuracy math.hypot() #85685
Copy link
Copy link
Closed
Labels
3.10 (EOL)end of lifeend of lifestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancementA feature request or enhancement
Description
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
assignee = None
closed_at = <Date 2020-09-15.00:15:26.344>
created_at = <Date 2020-08-10.01:14:14.750>
labels = ['type-feature', 'library', '3.10']
title = 'High accuracy math.hypot()'
updated_at = <Date 2020-10-02.00:19:15.829>
user = 'https://github.com/rhettinger'bugs.python.org fields:
activity = <Date 2020-10-02.00:19:15.829>
actor = 'rhettinger'
assignee = 'none'
closed = True
closed_date = <Date 2020-09-15.00:15:26.344>
closer = 'rhettinger'
components = ['Library (Lib)']
creation = <Date 2020-08-10.01:14:14.750>
creator = 'rhettinger'
dependencies = []
files = ['49380', '49399', '49424', '49439', '49448', '49484']
hgrepos = []
issue_num = 41513
keywords = ['patch']
message_count = 39.0
messages = ['375089', '375096', '375166', '375437', '375440', '375442', '375447', '375448', '375483', '375489', '375498', '375500', '375501', '375591', '375600', '375623', '375634', '375635', '375651', '375685', '375721', '375762', '375763', '375770', '375829', '375865', '375866', '375867', '375868', '375896', '376068', '376098', '376470', '376869', '376910', '376919', '376920', '377242', '377354']
nosy_count = 6.0
nosy_names = ['tim.peters', 'rhettinger', 'terry.reedy', 'mark.dickinson', 'vstinner', 'serhiy.storchaka']
pr_nums = ['21803', '21916', '21994', '22013', '22032', '22123', '22238', '22249', '22315', '22327', '22368']
priority = 'normal'
resolution = 'fixed'
stage = 'resolved'
status = 'closed'
superseder = None
type = 'enhancement'
url = 'https://bugs.python.org/issue41513'
versions = ['Python 3.10']Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
3.10 (EOL)end of lifeend of lifestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancementA feature request or enhancement
Activity
rhettinger commentedon Aug 10, 2020
I'd like to resurrect Serhiy's idea for using power-of-two scaling in math.hypot() rather than dividing each coordinate by the maximum value.
I drafted a patch. It looks to be a little faster than what we have now and is generally (but not always) more accurate.
For accuracy, the current approach has the advantage that for the largest coordinate, (x/max)**2 is exactly 1.0. If that element is much larger than the others, it overpowers the 1/2 ulp error in each of the other divisions.
In contrast, scaling by a power of two is always exact, but the squaring of the largest scaled coordinate is no longer exact.
The two approaches each have some cases that are more accurate than the other. I generated 10,000 samples. In 66% of the cases, the two results agree. In 26% of the cases, scaling by a power of two was more accurate. In the remaining 8%, the division by max was more accurate. The standard deviation of relative errors was better using power-of-two scaling. These results were consistent whether using 2, 3, or 20 dimensions.
As expected, multiplying by a power of two was a bit faster than dividing by an arbitrary float.
rhettinger commentedon Aug 10, 2020
Added a revised script for testing accuracy measured in ULPs. It shows an improvement for all dimensions tested, with the best for 2 dimensions. Also, the maximum error is now 1 ulp; formerly, it was 2 ulps.
======== 2 dimensions ========
div-by-max: [(-2.0, 14), (-1.0, 1721), (0.0, 6533), (1.0, 1722), (2.0, 10)]
scaled-by-2: [(-1.0, 841), (0.0, 8339), (1.0, 820)]
======== 3 dimensions ========
div-by-max: [(-2.0, 3), (-1.0, 1525), (0.0, 6933), (1.0, 1535), (2.0, 4)]
scaled-by-2: [(-1.0, 739), (0.0, 8515), (1.0, 746)]
======== 5 dimensions ========
div-by-max: [(-2.0, 2), (-1.0, 1377), (0.0, 7263), (1.0, 1358)]
scaled-by-2: [(-1.0, 695), (0.0, 8607), (1.0, 698)]
======== 10 dimensions ========
div-by-max: [(-2.0, 13), (-1.0, 1520), (0.0, 6873), (1.0, 1580), (2.0, 14)]
scaled-by-2: [(-1.0, 692), (0.0, 8605), (1.0, 703)]
======== 20 dimensions ========
div-by-max: [(-1.0, 1285), (0.0, 7310), (1.0, 1405)]
scaled-by-2: [(-1.0, 555), (0.0, 8822), (1.0, 623)]
mdickinson commentedon Aug 11, 2020
Fine by me in principle; I haven't had a chance to look at the code yet.
While we're doing this, any chance we could special-case the two-argument hypot to use the libm hypot directly? On many platforms the libm hypot will be correctly rounded, or close to it. There was a noticeable accuracy regression for two-argument hypot when hypot gained the ability to support multiple arguments.
rhettinger commentedon Aug 14, 2020
Yes, that is an easy amendment (see below).
I just tried it out on the macOs build but didn't see a change in accuracy from the current PR which uses lossless power-of-two-scaling and Neumaier summation.
In GCC's libquadmath implementation, the comments say that the error is less than 1 ulp, falling short of being correctly rounded within ±½ ulp.
If the platform hypots have the nearly the same accuracy as the current PR, it may make sense to skip the special case and opt for consistent cross-platform results.
==================================================================
======== 2 dimensions ========
platform hypot(): [(-1.0, 8398), (0.0, 83152), (1.0, 8450)]
scaled-by-2 : [(-1.0, 8412), (0.0, 83166), (1.0, 8422)]
rhettinger commentedon Aug 14, 2020
This paper addresses the topic directly: https://arxiv.org/pdf/1904.09481.pdf
I tried to reproduce the results shown in Table 1 of the paper. For clib hypot(), they get 1 ulp errors 12.91% of the time. On my build, using the same gaussian distribution, I get 14.08% for both the clang hypot() and for the current version of the PR, showing that the two are fundamentally the same.
rhettinger commentedon Aug 15, 2020
Per the referenced paper, here's what is involved in further reducing error:
rhettinger commentedon Aug 15, 2020
Update:
tim-one commentedon Aug 15, 2020
Cute: for any number of arguments, try computing h**2, then one at a time subtract a**2 (an argument squared) in descending order of magnitude. Call that (h**2 - a1**2 - a2**2 - ...) x.
Then
That should reduce errors too, although not nearly so effectively, since it's a cheap way of estimating (& correcting for) the discrepancy between sum(a**2) and h**2.
Note that "descending order" is important: it's trying to cancel as many leading bits as possible as early as possible, so that lower-order bits come into play.
Then again ... why bother? ;-)
tim-one commentedon Aug 15, 2020
But that doesn't really help unless the sum of squares was computed without care to begin with. Can do as well by skipping that but instead computing the original sum of squares in increasing order of magnitude.
Cheapest way I know of that "seemingly always" reproduces the Decimal result (when that's rounded back to float) combines fsum(), Veltkamp splitting, and the correction trick from the paper:
28 remaining items
tim-one commentedon Aug 25, 2020
One more implication: since the quality of the initial square root doesn't really much matter, instead of
at the end, it should work just as well (in fact, probably a little better, due to getting more beneficial cancellation) to do:
at the end. Although this crucially relies on the doing the last-line chain of subtracts and adds "left to right".
rhettinger commentedon Aug 29, 2020
New changeset 27de286 by Raymond Hettinger in branch 'master':
bpo-41513: Save unnecessary steps in the hypot() calculation (bpo-21994)
27de286
tim-one commentedon Aug 30, 2020
About test_frac.py, I changed the main loop like so:
That is, to display every case where the four float results weren't identical.
Result: nothing was displayed (although it's still running the n=1000 chunk, there's no sign that will change). None of these variations made any difference to results users actually get.
Even the "worst" of these reliably develops dozens of "good bits" beyond IEEE double precision, but invisibly (under the covers, with no visible effect on delivered results).
So if there's something else that speeds the code, perhaps it's worth pursuing, but we're already long beyond the point of getting any payback for pursuing accuracy.
rhettinger commentedon Sep 6, 2020
New changeset 67c998d by Raymond Hettinger in branch 'master':
bpo-41513: Expand comments and add references for a better understanding (GH-22123)
67c998d
rhettinger commentedon Sep 14, 2020
New changeset 457d4e9 by Raymond Hettinger in branch 'master':
bpo-41513: Add docs and tests for hypot() (GH-22238)
457d4e9
vstinner commentedon Sep 14, 2020
Multiple tests failed on x86 Gentoo Non-Debug with X 3.x:
https://buildbot.python.org/all/#/builders/58/builds/73
Example of failure:
======================================================================
FAIL: testHypotAccuracy (test.test_math.MathTests) (hx='0x1.10106eb4b44a2p+29', hy='0x1.ef0596cdc97f8p+29')
----------------------------------------------------------------------
(I didn't investigate the bug, I just report the failure.)
rhettinger commentedon Sep 15, 2020
New changeset 95a8a0e by Raymond Hettinger in branch 'master':
bpo-41513: Remove broken tests that fail on Gentoo (GH-22249)
95a8a0e
rhettinger commentedon Sep 15, 2020
I saw that the Gentoo bot reacted badly to the new tests, so I reverted them.
rhettinger commentedon Sep 21, 2020
New changeset bc6b7fa by Raymond Hettinger in branch 'master':
bpo-41513: Add accuracy tests for math.hypot() (GH-22327)
bc6b7fa
rhettinger commentedon Sep 23, 2020
New changeset 438e9fc by Raymond Hettinger in branch 'master':
bpo-41513: Improve order of adding fractional values. Improve variable names. (GH-22368)
438e9fc