Skip to content

Commit 2b8c3f2

Browse files
committed
feat(Lib/math): nextafter, ulp (with test)
1 parent ec5bd95 commit 2b8c3f2

File tree

6 files changed

+304
-1
lines changed

6 files changed

+304
-1
lines changed

src/pylib/Lib/math.nim

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,10 @@ func comb*(n, k: int): int =
272272
nk = chkValNe k
273273
n_math.comb(nn, nk)
274274

275-
# TODO: nextafter ulp
275+
func nextafter*[F: SomeFloat](x, y: F): F{.pysince(3,9).} = n_math.nextafter(x, y)
276+
func nextafter*[F: SomeFloat](x, y: F; steps: int|uint64): F{.pysince(3,12).} =
277+
n_math.nextafter(x, y, steps)
278+
func ulp*[F: SomeFloat](x: F): F{.pysince(3,9).} = n_math.ulp(x)
276279

277280
expN gcd
278281
expN lcm
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
2+
3+
from ./isX import isnan
4+
5+
proc nextafter*(x, y: float;
6+
usteps: uint64): float =
7+
## [clinic input]
8+
## math.nextafter
9+
##
10+
## x: double
11+
## y: double
12+
## /
13+
##
14+
## steps: object = None
15+
##
16+
## Return the floating-point value the given number of steps after x towards y.
17+
##
18+
## If steps is not specified or is None, it defaults to 1.
19+
##
20+
## Raises a TypeError, if x or y is not a double, or if steps is not an integer.
21+
## Raises ValueError if steps is negative.
22+
## [clinic start generated code]
23+
## [clinic end generated code: output=cc6511f02afc099e input=7f2a5842112af2b4]
24+
when defined(aix):
25+
if x == y:
26+
## On AIX 7.1, libm nextafter(-0.0, +0.0) returns -0.0.
27+
## Bug fixed in bos.adt.libm 7.2.2.0 by APAR IV95512.
28+
return (y)
29+
if isnan(x):
30+
return (x)
31+
if isnan(y):
32+
return (y)
33+
34+
## Conveniently, uint64_t and double have the same number of bits
35+
## on all the platforms we care about.
36+
## So if an overflow occurs, we can just use UINT64_MAX.
37+
38+
if usteps == 0:
39+
return x
40+
if isnan(x):
41+
return x
42+
if isnan(y):
43+
return y
44+
type
45+
pun {.union.} = object # XXX: TODO: union object cannot run when nimvm
46+
f: float64
47+
i: uint64
48+
49+
var
50+
ux = pun(f: x)
51+
uy = pun(f: y)
52+
if ux.i == uy.i:
53+
return x
54+
const sign_bit = 1'u64 shl 63
55+
let
56+
ax: uint64 = ux.i and not sign_bit
57+
ay: uint64 = uy.i and not sign_bit
58+
## opposite signs
59+
if bool((ux.i xor uy.i) and sign_bit):
60+
## NOTE: ax + ay can never overflow, because their most significant bit
61+
## ain't set.
62+
if ax + ay <= usteps:
63+
return uy.f
64+
## This comparison has to use <, because <= would get +0.0 vs -0.0
65+
## wrong.
66+
elif ax < usteps:
67+
let res = pun(i: (uy.i and sign_bit) or (usteps - ax))
68+
return res.f
69+
else:
70+
dec(ux.i, usteps)
71+
return ux.f
72+
elif ax > ay: ## same sign
73+
if ax - ay >= usteps:
74+
dec(ux.i, usteps)
75+
return ux.f
76+
else:
77+
return uy.f
78+
else:
79+
if ay - ax >= usteps:
80+
inc(ux.i, usteps)
81+
return ux.f
82+
else:
83+
return uy.f
84+
85+
when culonglong is_not uint64:
86+
proc nextafter*(x, y: float;
87+
usteps_ull: culonglong): float =
88+
let usteps =
89+
if usteps_ull >= UINT64_MAX:
90+
## This branch includes the case where an error occurred, since
91+
## (unsigned long long)(-1) = ULLONG_MAX >= UINT64_MAX. Note that
92+
## usteps_ull can be strictly larger than UINT64_MAX on a machine
93+
## where unsigned long long has width > 64 bits.
94+
UINT64_MAX
95+
else:
96+
cast[uint64](usteps_ull)
97+
nextafter(x, y, uint64 ustep)
98+
99+
func nextafter*(x, y: float;
100+
steps: int): float =
101+
if steps < 0:
102+
raise newException(ValueError, "steps must be a non-negative integer")
103+
nextafter(x, y, uint64 steps)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
3+
from ./platformUtils import CLike, clikeOr
4+
import ./patch/nextafter as n_nextafterLib
5+
import ./nextafter_step
6+
from ./isX import isnan, isinf
7+
8+
when CLike:
9+
{.push header: "<math.h>".}
10+
proc c_nextafter(frm, to: c_double): c_double{.importc: "nextafter".}
11+
proc c_nextafter(frm, to: c_float): c_float{.importc: "nextafterf".}
12+
{.pop.}
13+
14+
func nextafter*[F: SomeFloat](x, y: F): F =
15+
clikeOr(
16+
c_nextafter(x, y),
17+
n_nextafterLib.nextafter(x, y)
18+
)
19+
20+
func nextafter*[F: SomeFloat](x, y: F; steps: int|uint64): F =
21+
nextafter_step.nextafter(x, y, steps)
22+
23+
func ulp*[F: SomeFloat](x: F): F =
24+
bind nextafter
25+
if isnan(x): return x
26+
let x = abs(x)
27+
if isinf(x): return x
28+
result = nextafter(x, Inf)
29+
if isinf(result):
30+
# special case: x is the largest positive representable float
31+
result = nextafter(x, NegInf)
32+
return x - result
33+
result -= x
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
2+
##[
3+
translated from https://github.com/scijs/nextafter/blob/master/nextafter.js
4+
]##
5+
6+
#[
7+
The MIT License (MIT)
8+
9+
Copyright (c) 2013 Mikola Lysenko
10+
11+
Permission is hereby granted, free of charge, to any person obtaining a copy
12+
of this software and associated documentation files (the "Software"), to deal
13+
in the Software without restriction, including without limitation the rights
14+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15+
copies of the Software, and to permit persons to whom the Software is
16+
furnished to do so, subject to the following conditions:
17+
18+
The above copyright notice and this permission notice shall be included in
19+
all copies or substantial portions of the Software.
20+
21+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27+
THE SOFTWARE.
28+
]#
29+
30+
from ./ldexp_frexp/fromWords import fromWords
31+
from ./ldexp_frexp/toWords import toWords
32+
from std/math import pow, isNaN
33+
34+
const
35+
UINT_MAX = high uint32
36+
dbl_SMALLEST_DENORM = pow(2.0, -1074)
37+
#flt_SMALLEST_DENORM = pow(2, )
38+
39+
template SMALLEST_DENORM[T](t: typedesc[T]): T =
40+
when T is float64: dbl_SMALLEST_DENORM
41+
else: {.error: "not impl".} # XXX: rely on from/toWords, currently they're float64 only
42+
43+
func nextafter*[F](x, y: F): F =
44+
if isNaN(x) or isNaN(y):
45+
return NaN
46+
if x == y:
47+
return x
48+
if x == 0:
49+
if y < 0:
50+
return -F.SMALLEST_DENORM
51+
else:
52+
return F.SMALLEST_DENORM
53+
var (hi, lo) = toWords(x)
54+
if (y > x) == (x > 0):
55+
if(lo == UINT_MAX):
56+
hi += 1
57+
lo = 0
58+
else:
59+
lo += 1
60+
else:
61+
if(lo == 0):
62+
lo = UINT_MAX
63+
hi -= 1
64+
else:
65+
lo -= 1
66+
67+
return fromWords(lo, hi)

src/pylib/Lib/n_math.nim

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ from ./math_impl/errnoUtils import
3131
from ./math_impl/ldexp import c_ldexp
3232
from ./math_impl/cbrt import cbrt
3333
import ./math_impl/frexp as frexpLib
34+
import ./math_impl/nextafter_ulp
3435
from ./errno import ERANGE, EDOM
3536

3637
export cbrt
38+
export nextafter, ulp
3739

3840
macro impPatch(sym) =
3941
#import ./math_impl/patch/sym

src/pylib/Lib/test/test_math.nim

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import ./import_utils
55
importPyLib math
6+
importPyLib sys
67
pyimport unittest
78
from std/unittest import suiteStarted, TestStatus, testStarted, suiteEnded, checkpoint, fail, TestResult,
89
suite, test, check, expect
@@ -197,6 +198,100 @@ suite "classify":
197198
not (isfinite(F_INF))
198199
not (isfinite(F_NINF))
199200

201+
suite "nextafter_ulp":
202+
template assertEqualSign(a, b) =
203+
let
204+
sa = copysign(1.0, a)
205+
sb = copysign(1.0, b)
206+
check sa == sb
207+
template assertIsNaN(x) =
208+
check isnan(x)
209+
test "nextafter":
210+
#@requires_IEEE_754
211+
def test_nextafter():
212+
# around 2^52 and 2^63
213+
assertEqual(math.nextafter(4503599627370496.0, -INF),
214+
4503599627370495.5)
215+
assertEqual(math.nextafter(4503599627370496.0, INF),
216+
4503599627370497.0)
217+
assertEqual(math.nextafter(9223372036854775808.0, 0.0),
218+
9223372036854774784.0)
219+
assertEqual(math.nextafter(-9223372036854775808.0, 0.0),
220+
-9223372036854774784.0)
221+
222+
# around 1.0
223+
assertEqual(math.nextafter(1.0, -INF),
224+
float_fromhex("0x1.fffffffffffffp-1"))
225+
assertEqual(math.nextafter(1.0, INF),
226+
float_fromhex("0x1.0000000000001p+0"))
227+
assertEqual(math.nextafter(1.0, -INF, steps=1),
228+
float_fromhex("0x1.fffffffffffffp-1"))
229+
assertEqual(math.nextafter(1.0, INF, steps=1),
230+
float_fromhex("0x1.0000000000001p+0"))
231+
assertEqual(math.nextafter(1.0, -INF, steps=3),
232+
float_fromhex("0x1.ffffffffffffdp-1"))
233+
assertEqual(math.nextafter(1.0, INF, steps=3),
234+
float_fromhex("0x1.0000000000003p+0"))
235+
236+
# x == y: y is returned
237+
for steps in range(1, 5):
238+
assertEqual(math.nextafter(2.0, 2.0, steps=steps), 2.0)
239+
assertEqualSign(math.nextafter(-0.0, +0.0, steps=steps), +0.0)
240+
assertEqualSign(math.nextafter(+0.0, -0.0, steps=steps), -0.0)
241+
242+
# around 0.0
243+
smallest_subnormal = sys.float_info.min * sys.float_info.epsilon
244+
assertEqual(math.nextafter(+0.0, INF), smallest_subnormal)
245+
assertEqual(math.nextafter(-0.0, INF), smallest_subnormal)
246+
assertEqual(math.nextafter(+0.0, -INF), -smallest_subnormal)
247+
assertEqual(math.nextafter(-0.0, -INF), -smallest_subnormal)
248+
assertEqualSign(math.nextafter(smallest_subnormal, +0.0), +0.0)
249+
assertEqualSign(math.nextafter(-smallest_subnormal, +0.0), -0.0)
250+
assertEqualSign(math.nextafter(smallest_subnormal, -0.0), +0.0)
251+
assertEqualSign(math.nextafter(-smallest_subnormal, -0.0), -0.0)
252+
253+
# around infinity
254+
largest_normal = sys.float_info.max
255+
assertEqual(math.nextafter(INF, 0.0), largest_normal)
256+
assertEqual(math.nextafter(-INF, 0.0), -largest_normal)
257+
assertEqual(math.nextafter(largest_normal, INF), INF)
258+
assertEqual(math.nextafter(-largest_normal, -INF), -INF)
259+
260+
# NaN
261+
assertIsNaN(math.nextafter(NAN, 1.0))
262+
assertIsNaN(math.nextafter(1.0, NAN))
263+
assertIsNaN(math.nextafter(NAN, NAN))
264+
265+
assertEqual(1.0, math.nextafter(1.0, INF, steps=0))
266+
expect(ValueError):
267+
discard math.nextafter(1.0, INF, steps = -1)
268+
test_nextafter()
269+
test "ulp":
270+
const FLOAT_MAX = high float64
271+
#@requires_IEEE_754
272+
def test_ulp():
273+
assertEqual(math.ulp(1.0), sys.float_info.epsilon)
274+
# use int ** int rather than float ** int to not rely on pow() accuracy
275+
assertEqual(math.ulp(2.0 ** 52), 1.0)
276+
assertEqual(math.ulp(2.0 ** 53), 2.0)
277+
assertEqual(math.ulp(2.0 ** 64), 4096.0)
278+
279+
# min and max
280+
assertEqual(math.ulp(0.0),
281+
sys.float_info.min * sys.float_info.epsilon)
282+
assertEqual(math.ulp(FLOAT_MAX),
283+
FLOAT_MAX - math.nextafter(FLOAT_MAX, -INF))
284+
285+
# special cases
286+
assertEqual(math.ulp(INF), INF)
287+
assertIsNaN(math.ulp(math.nan))
288+
289+
# negative number: ulp(-x) == ulp(x)
290+
for x in [0.0, 1.0, 2.0 ** 52, 2.0 ** 64, INF]:
291+
#with subTest(x=x):
292+
assertEqual(math.ulp(-x), math.ulp(x))
293+
test_ulp()
294+
200295
suite "ldexp":
201296
test "static":
202297
const f = ldexp(1.0, 2)

0 commit comments

Comments
 (0)