# Floating-point accuracy visualization example: the ULP error for each math function coming with Julia

**URL:** https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392
**Category:** Numerics
**Tags:** error, visualization, numerics, approximation, accuracy
**Created:** [February 1, 2026, 9:02pm UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392 "2026-02-01T21:02:59Z")
**Posts on this page:** 20
**Page:** 1

<div class="post-metadata">

### Author: ![nsajko](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nsajko/32/221187_2.png) [@nsajko](https://discourse.julialang.org/u/nsajko)
#### Post date: [February 1, 2026, 9:02pm UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/1 "2026-02-01T21:02:59Z")

</div>

## Introduction

The implementation of a math function, such as `cos(::Float64)` in Julia, should be fast, but also needs to be at least somewhat accurate. Here I propose a method for visualizing empirical accuracy data.

Consider the discrepancy between the exact value of a univariate real function, such as the sine, and the approximate value that is returned by a call such as `sin(0.3)`. By putting an interval that subsets the domain of the function on the x-axis of a plot, and the value of this discrepancy, the _error_, onto the y-axis of the same plot, it might be possible to tell on which part of the function’s domain is the implementation less accurate than on other parts. That is, it might be possible to read off the plot the worst-case regions for the approximation.

I choose the error measured in _units in the last place_/_units of least precision_ (ULPs) as the type of error to visualize here. Other types of error that are often used are _absolute error_, which is not usually appropriate for floating-point numbers, and _relative error_, which has these drawbacks to measuring the error in ULPs:

- it is usually less immediately useful and less intuitive

- plotting software tends to have trouble with the tiny values

One advantage of the error in ULPs is that it has some convenient and easy interpretations in the context of floating-point numbers. For example:

- If the error between the exact value and the approximate (floating-point) value is less than **half** , the approximate value is the number closest to the exact value among the numbers belonging to that floating-point representation. The technical term is _correctly rounded_.

- If the error is merely less than **one** , the approximate value is one of the two floating-point numbers closest to the exact value. The technical term is _faithfully rounded_ (although faithful rounding is not technically a rounding).

If we tried to, say, evaluate the error in ULPs on an evenly-spaced grid on the chosen interval, the plot would just look like noise if each evaluation was plotted as a data point. However, given that the worst cases are what is most interesting, it might be possible to achieve a comprehensible visualization by giving up on visualizing the best cases. In other words, we’re interested in the upward spikes of the error, and by eliding the downward spikes it might be possible to make the plot a lot less noisy.

To accomplish this, the approach I choose here is vaguely similar to downsampling/decimation, from signal analysis: take n values, where n is some large positive integer, and represent them on the plot by an aggregate value: the maximum value among the n points (in this case). In a signal analysis context, it is also common to apply a _lowpass filter_ to reduce noise/high-frequency components of the signal, before the decimation itself. This helps prevent artifacts in the resulting output. In this case, a _sliding window maximum_ seems like an appropriate lowpass filter to smooth out the data before decimation, to prevent artifacts.

## Julia app on Github

The app used for creating these visualizations is published on Github:

- [GitHub - JuliaMath/VisualizeULPError.jl: Visualize the numerical error of a real univariate function](https://github.com/JuliaMath/VisualizeULPError.jl)

Neither the package, nor the app it exposes, are registered as of writing this.

## The plots

Might be useful to both contributors and users of Julia to better understand where there’s room for improvement in the current implementations. In the cases of some functions, though, the worst case error spikes are difficult or impossible to fix efficiently, without reaching for some arbitrary-precision implementation like MPFR (`BigFloat`) or Arb.

### `acos(::Float64)`

 ![acos](https://global.discourse-cdn.com/julialang/original/3X/9/9/991ae20fd9f6c999715e7d59624ee1db1e38a416.svg)

### `acosd(::Float64)`

 ![acosd](https://global.discourse-cdn.com/julialang/original/3X/6/d/6d83f8d6b3b382ea7fd8dfef3749548053f0c7a6.svg)

### `acosh(::Float64)`

 ![acosh](https://global.discourse-cdn.com/julialang/original/3X/4/9/491106d821bdeabc2d846209088201e91b47d4f1.svg)

### `acot(::Float64)`

 ![acot](https://global.discourse-cdn.com/julialang/original/3X/4/4/448c73a922551b05fc209469ed1c29aa04cfa22b.svg)

### `acotd(::Float64)`

 ![acotd](https://global.discourse-cdn.com/julialang/original/3X/0/1/0103d78d3f46761ea6ff71d8fb84dca48f32e625.svg)

### `acoth(::Float64)`

 ![acoth](https://global.discourse-cdn.com/julialang/original/3X/1/7/17d4d6b704f4258e3f0735b1a24e2dbd9258d02c.svg)

### `acsc(::Float64)`

 ![acsc](https://global.discourse-cdn.com/julialang/original/3X/d/d/ddf2db85e9bc2729c8dac9d190cc229e05751073.svg)

### `acscd(::Float64)`

 ![acscd](https://global.discourse-cdn.com/julialang/original/3X/d/1/d1f9218f62c4c40d4f7734baf74722c35680a380.svg)

### `acsch(::Float64)`

 ![acsch](https://global.discourse-cdn.com/julialang/original/3X/b/1/b1ce0523a853323b9f0725a697d6638045c03dc6.svg)

### `asec(::Float64)`

 ![asec](https://global.discourse-cdn.com/julialang/original/3X/d/d/dd6b429938774d42bf92650dfb5b97376423513e.svg)

### `asecd(::Float64)`

 ![asecd](https://global.discourse-cdn.com/julialang/original/3X/4/d/4d3d2e66541855039281ab9d9ebaed8d7dd777b8.svg)

### `asech(::Float64)`

 ![asech](https://global.discourse-cdn.com/julialang/original/3X/e/3/e3144a7d4bd4c79895dcd8d6c3ade24c1c583608.svg)

### `asin(::Float64)`

 ![asin](https://global.discourse-cdn.com/julialang/original/3X/7/3/731bb60be26ae25381b476f050c694dfd7dd6826.svg)

### `asind(::Float64)`

 ![asind](https://global.discourse-cdn.com/julialang/original/3X/7/f/7f72281c602d38d5cd665eea4242a938e3dd440a.svg)

### `asinh(::Float64)`

 ![asinh](https://global.discourse-cdn.com/julialang/original/3X/4/5/4565a51dd92b48e1cc6b7662035074ffb22e3510.svg)

### `atan(::Float64)`

 ![atan](https://global.discourse-cdn.com/julialang/original/3X/f/3/f3323e3134a8cb35430926aec0c40f4fb7472aa8.svg)

### `atand(::Float64)`

 ![atand](https://global.discourse-cdn.com/julialang/original/3X/0/7/07f4d7cfa5d33ffa9ded94950c266db12eb2a1db.svg)

### `atanh(::Float64)`

 ![atanh](https://global.discourse-cdn.com/julialang/original/3X/b/7/b76c52b94a2b6209857229b8920568183c0b2ba6.svg)

### `cos(::Float64)`

 ![cos](https://global.discourse-cdn.com/julialang/original/3X/7/6/76af0b2dae2605037d01c0d1a3862386581afa13.svg)

### `cosc(::Float64)`

 ![cosc](https://global.discourse-cdn.com/julialang/original/3X/3/b/3b895b7e79f9f32f0a34650bf6fa9caea84d669b.svg)

### `cosd(::Float64)`

 ![cosd](https://global.discourse-cdn.com/julialang/original/3X/b/d/bd1e528f20d37812929d089b688144ac3c02e5f1.svg)

### `cosh(::Float64)`

 ![cosh](https://global.discourse-cdn.com/julialang/original/3X/5/1/5155a62473400a2e62aa343e480829aa05f80a3e.svg)

### `cospi(::Float64)`

 ![cospi](https://global.discourse-cdn.com/julialang/original/3X/3/c/3c7e23689c44113073c5e66292ad9bab753ac556.svg)

### `cot(::Float64)`

 ![cot](https://global.discourse-cdn.com/julialang/original/3X/8/e/8ef7dd95cadd8a810e51a9a2309934f2fba911aa.svg)

### `cotd(::Float64)`

 ![cotd](https://global.discourse-cdn.com/julialang/original/3X/0/6/067087958bea6a6f5ee1fc80984c71a6b6353acd.svg)

### `coth(::Float64)`

 ![coth](https://global.discourse-cdn.com/julialang/original/3X/8/7/8772e5c7218d763cbe570d4b5baefe41dbb63623.svg)

### `csc(::Float64)`

 ![csc](https://global.discourse-cdn.com/julialang/original/3X/2/6/266d596c850cf906a896f5485528151450386b49.svg)

### `cscd(::Float64)`

 ![cscd](https://global.discourse-cdn.com/julialang/original/3X/d/2/d2f6564fd420a621ed5a81453fa3621b086844ce.svg)

### `csch(::Float64)`

 ![csch](https://global.discourse-cdn.com/julialang/original/3X/f/b/fb447b2c018206a0d6ad4c66a6dd2285afd9e827.svg)

### `deg2rad(::Float64)`

 ![deg2rad](https://global.discourse-cdn.com/julialang/original/3X/0/f/0f7974023b814055348968627d510b20a24fe644.svg)

### `exp(::Float64)`

 ![exp](https://global.discourse-cdn.com/julialang/original/3X/b/c/bcc0ac2d9c0b0d8682470068a1da95e931d1774e.svg)

### `exp10(::Float64)`

 ![exp10](https://global.discourse-cdn.com/julialang/original/3X/b/a/ba5f73214b5715fdf0386b947f2ae6e01455eb58.svg)

### `exp2(::Float64)`

 ![exp2](https://global.discourse-cdn.com/julialang/original/3X/5/f/5fcc3117169771e460fee7c14c201f2d0ef8904c.svg)

### `expm1(::Float64)`

 ![expm1](https://global.discourse-cdn.com/julialang/original/3X/a/3/a36deb20eadcc563b00e8eee2492c1e6cf3a0893.svg)

### `log(::Float64)`

 ![log](https://global.discourse-cdn.com/julialang/original/3X/d/6/d6009ba469842bae0a5b6a1814ee80ac926bed9a.svg)

### `log10(::Float64)`

 ![log10](https://global.discourse-cdn.com/julialang/original/3X/1/a/1a460b180d75e91dd34d78193da1d3c36022133a.svg)

### `log1p(::Float64)`

 ![log1p](https://global.discourse-cdn.com/julialang/original/3X/2/3/23ae48425f0eeef9904ff5f07de33be728734be9.svg)

### `log2(::Float64)`

 ![log2](https://global.discourse-cdn.com/julialang/original/3X/6/9/69662c9d2ccfb05e7df7d51c344eddd7f60ee6f2.svg)

### `rad2deg(::Float64)`

 ![rad2deg](https://global.discourse-cdn.com/julialang/original/3X/5/c/5c52d60ac9f7ec5958655a040a2cb6ca2ae29973.svg)

### `sec(::Float64)`

 ![sec](https://global.discourse-cdn.com/julialang/original/3X/4/a/4a80468d46abf9d6b69094b885590b1e961603ce.svg)

### `secd(::Float64)`

 ![secd](https://global.discourse-cdn.com/julialang/original/3X/7/b/7b5b3ffb92e66c300d5505945987cc29438ed23f.svg)

### `sech(::Float64)`

 ![sech](https://global.discourse-cdn.com/julialang/original/3X/9/4/9482ab8917aab88dd5a2cf66993050b998703576.svg)

### `sin(::Float64)`

 ![sin](https://global.discourse-cdn.com/julialang/original/3X/7/d/7d99af10b7e54c39c3e56dbc55234484c429383f.svg)

### `sinc(::Float64)`

 ![sinc](https://global.discourse-cdn.com/julialang/original/3X/f/d/fdbaf41d6c35e93dce4b97d8ac9bb2cdfddf4ef4.svg)

### `sind(::Float64)`

 ![sind](https://global.discourse-cdn.com/julialang/original/3X/b/8/b880d370949d43b7d69b938dee423e7e39578bf8.svg)

### `sinh(::Float64)`

 ![sinh](https://global.discourse-cdn.com/julialang/original/3X/3/4/34ffbf1237e146f4c137320b4eb4a8f51369cd21.svg)

### `sinpi(::Float64)`

 ![sinpi](https://global.discourse-cdn.com/julialang/original/3X/3/7/37897a9326d5a5165f2e6f1a4b14cee3c5b1a503.svg)

### `tan(::Float64)`

 ![tan](https://global.discourse-cdn.com/julialang/original/3X/0/7/075c6ad4cd9dbb2ff3b5d22b66c4911b5bc01df2.svg)

### `tand(::Float64)`

 ![tand](https://global.discourse-cdn.com/julialang/original/3X/2/2/22c2d8e9963614d5d40c1693b5c3ce7464580fd0.svg)

### `tanh(::Float64)`

 ![tanh](https://global.discourse-cdn.com/julialang/original/3X/5/b/5be41d8ce3b5d75bffb3ff13f26972b6912748f0.svg)

### `tanpi(::Float64)`

 ![tanpi](https://global.discourse-cdn.com/julialang/original/3X/7/5/757c2ebfe3f85eaf585d26f78a220f372dfa004e.svg)

## Miscellaneous

### PRs featuring this visualization approach

- Julia itself

- Julia package **LogExpFunctions.jl**

### Version, platform info

```julia-repl
julia> versioninfo()
Julia Version 1.14.0-DEV.1670
Commit 6e64d0c3442 (2026-02-02 03:21 UTC)
Build Info:
  Official https://julialang.org release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 8 × AMD Ryzen 3 5300U with Radeon Graphics
  WORD_SIZE: 64
  LLVM: libLLVM-20.1.8 (ORCJIT, znver2)
  GC: Built with stock GC
Threads: 5 default, 1 interactive, 5 GC (on 8 virtual cores)

```

---

<div class="post-metadata">

### Author: ![jebej](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jebej/32/1784_2.png) [@jebej](https://discourse.julialang.org/u/jebej)
#### Post date: [February 1, 2026, 11:54pm UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/2 "2026-02-01T23:54:59Z")

</div>

Nice! But the plots are hard to see due to the low contrast, low resolution and small text, if it’s not too much trouble, I’m sure an svg (or higher res) version with better sizing and colors would be appreciated!

---

<div class="post-metadata">

### Author: ![OlivierHnt](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/olivierhnt/32/6227_2.png) [@OlivierHnt](https://discourse.julialang.org/u/OlivierHnt)
#### Post date: [February 2, 2026, 1:36am UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/4 "2026-02-02T01:36:06Z")

</div>

Interesting, thank you for sharing.  
We recently had a conversation about ULPs over at [IntervalArithmetic](https://github.com/JuliaIntervals/IntervalArithmetic.jl).  
We used to have a rounding mode based on `prevfloat`/`nextfloat` (fast, not tight, but GPU compatible), though we ultimately pulled the plug because determining the ULPs turned out to be tricky and architecture-dependent.

I am not an export on this subject, though Julia itself provides implementations of some functions:

1. Where can we find the list of implemented functions?

2. For these functions, this means that the ULP is architecture independent, correct?

3. Do any of these implementations provide proven bounds on the ULP error?  
I would not expect _correct rounding_ in general, but even a pessimistic, certified upper bound on the ULP would already be very useful for our purposes.  
Maybe one day we can even get an implementation of [CORE-Math](https://core-math.gitlabpages.inria.fr) functions.

---

<div class="post-metadata">

### Author: ![PatrickHaecker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/patrickhaecker/32/222891_2.png) [@PatrickHaecker](https://discourse.julialang.org/u/PatrickHaecker)
#### Post date: [February 3, 2026, 4:56am UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/5 "2026-02-03T04:56:16Z")

</div>

This sounds interesting. Am I the only one who can’t see the plots? I only see an “X” below each function name and I thought “X” never, ever marks the spot.

---

<div class="post-metadata">

### Author: ![nsajko](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nsajko/32/221187_2.png) [@nsajko](https://discourse.julialang.org/u/nsajko)
#### Post date: [February 3, 2026, 7:30am UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/6 "2026-02-03T07:30:02Z")

</div>

> [@PatrickHaecker](#):
>
> Am I the only one who can’t see the plots?

Yeah, the post is ~~not done yet~~ 😅

For context, this was originally a blog post on the Julia Forem. However Forem does not accept SVG, so I provided the plots as PNG. However, Forem downscaled all the images, which is something I only realized after publishing the Forem post and linking to it from here. Since then I got sidetracked by a possible Julia bug, so I haven’t gotten around to regenerating the plots.

EDIT: the post is finished now, excuse the confusion.

---

<div class="post-metadata">

### Author: ![nsajko](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nsajko/32/221187_2.png) [@nsajko](https://discourse.julialang.org/u/nsajko)
#### Post date: [February 3, 2026, 4:39pm UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/7 "2026-02-03T16:39:51Z")

</div>

> [@jebej](#):
>
> But the plots are hard to see due to the low contrast, low resolution and small text, if it’s not too much trouble, I’m sure an svg (or higher res) version with better sizing and colors would be appreciated!

The images are now in SVG. Hope it is OK now. Could also have titled the plots and labeled the axes, now that I think about it 🤷‍♂️

---

<div class="post-metadata">

### Author: ![nsajko](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nsajko/32/221187_2.png) [@nsajko](https://discourse.julialang.org/u/nsajko)
#### Post date: [February 3, 2026, 4:52pm UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/8 "2026-02-03T16:52:18Z")

</div>

> [@OlivierHnt](#):
>
> determining the ULPs turned out to be tricky and architecture-dependent.

Can you link some relevant discussion? I’m not completely sure what you are referring to.

A possible explanation is how the result of `muladd(::Float64, ::Float64, ::Float64)` is architecture-dependent, because modern platforms have a fused multiply-add (FMA) instruction, so `muladd` may be implemented with `fma`, but on older architectures `muladd` may suffer from double rounding.

> [@OlivierHnt](#):
>
> Where can we find the list of implemented functions?

The docs: [Mathematics · The Julia Language](https://docs.julialang.org/en/v1/base/math/)

> [@OlivierHnt](#):
>
> For these functions, this means that the ULP is architecture independent, correct?

Are you asking about the approximation error? If so, yeah, as mentioned just now above. An arch that lacks FMA may suffer from worse accuracy, but modern platforms do have an FMA. The Julia test suite adapts like this:

> <https://github.com/JuliaLang/julia/blob/8f91f510cf8c54778680ac461c23bdb1f6c9c47a/test/math.jl#L17-L29>

If you’re, on the other hand, just asking about how to calculate the ULP error between `a` and `b`, the formula is basically `(b - a) / eps(a)`. See the entire implementation here:

[https://github.com/nsajko/VisualizeULPError.jl/blob/368d4291b0fa37241e3af1c7b340ee6f9dbf7e37/src/ULPError.jl](https://github.com/nsajko/VisualizeULPError.jl/blob/368d4291b0fa37241e3af1c7b340ee6f9dbf7e37/src/ULPError.jl)

> [@OlivierHnt](#):
>
> Do any of these implementations provide proven bounds on the ULP error?

Pretty sure the answer is no.

---

<div class="post-metadata">

### Author: ![GeorgeGkountouras](https://avatars.discourse-cdn.com/v4/letter/g/77aa72/32.png) [@GeorgeGkountouras](https://discourse.julialang.org/u/GeorgeGkountouras)
#### Post date: [February 3, 2026, 7:53pm UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/9 "2026-02-03T19:53:30Z")

</div>

Cool! Does it work for arbitrary Julia code e.g. `my_cos()` ? What about C functions?

---

<div class="post-metadata">

### Author: ![woclass](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/woclass/32/212699_2.png) [@woclass](https://discourse.julialang.org/u/woclass)
#### Post date: [February 4, 2026, 4:58am UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/10 "2026-02-04T04:58:21Z")

</div>

I did some similar things for Bessels.jl, but just use relative error.

[Relative error plot for Besseli(nu, x) · JuliaMath/Bessels.jl · Discussion #126](https://github.com/JuliaMath/Bessels.jl/discussions/126)

 ![image](https://global.discourse-cdn.com/julialang/original/3X/5/4/549648b5e3582aa7444be5dfd24868cc2e5092e2.jpeg)

 ![image](https://global.discourse-cdn.com/julialang/original/3X/4/8/48b812cf6be4176ab09516dbf5447495f1cf3916.jpeg)

---

<div class="post-metadata">

### Author: ![OlivierHnt](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/olivierhnt/32/6227_2.png) [@OlivierHnt](https://discourse.julialang.org/u/OlivierHnt)
#### Post date: [February 4, 2026, 6:42am UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/11 "2026-02-04T06:42:16Z")

</div>

Thx! Huh so all of these listed functions are implemented in pure Julia.

I was thinking of how, e.g., the gnu lib is architecture dependent.  
So the only safe cases are the functions which are part of the IEEE 754. Anything beyond this, I suppose, would have to be a separate Julia package.

---

<div class="post-metadata">

### Author: ![nsajko](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nsajko/32/221187_2.png) [@nsajko](https://discourse.julialang.org/u/nsajko)
#### Post date: [February 4, 2026, 6:52am UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/12 "2026-02-04T06:52:55Z")

</div>

> [@GeorgeGkountouras](#):
>
> Cool!

Thank you!

> [@GeorgeGkountouras](#):
>
> Does it work for arbitrary Julia code e.g. `my_cos()` ?

Sure, just define the function inline when calling the app. I had to make slight adjustments (`invokelatest`) to make that work. The readme on Github now includes the following example:

> To measure the error of an arbitrary function, define it when providing the command options:
> 
> ```sh
> visualize_ulp_error plot 'let sin_fast
> function sin_fast(x::BigFloat) # accurate reference
> sin(x)
> end
> function sin_fast(x::Float64) # fast approximation of `sin(x)` for `abs(x) ≤ 0.125`
> # Found with Sollya, using this command: `fpminimax(sin(x), [|1, 3, 5|], [|D...|], [0; 0.125]);`.
> p = (0.9999999999763268, -0.16666663941291426, 0.008328683245851542)
> x * evalpoly(x * x, p) # odd polynomial composed from simpler polynomials
> end
> (;
> parent_dir = "/home/nsajko/ulp_error_plots",
> downsampled_length = 650,
> factor = 6000,
> window_size = 10,
> bf_precision = 140,
> no_scoped_values = true,
> width = 1100px,
> height = 500px,
> func = sin_fast,
> itv = (-0.125, 0.125),
> )
> end'
> 
> ```

The result:

 ![sin_fast](https://global.discourse-cdn.com/julialang/original/3X/0/1/01892edfe8c4478b85fdefbf609ad55bdf8d9817.svg)

A catch is that a Julia app can not(?) load more Julia packages. So, if you want to measure the error of a function defined in some package, you would need to either:

- Redefine the function inline when calling the app, without referring to any package.

- Use the package directly from the REPL, instead of using the Julia app. There is no supported [API](https://github.com/JuliaMath/VisualizeULPError.jl/issues/13) yet, but if you just want to hack around, something like this works at the moment:

> [@GeorgeGkountouras](#):
>
> What about C functions?

I suppose one could wrap a C function using `@ccall`.

---

<div class="post-metadata">

### Author: ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)
#### Post date: [February 4, 2026, 7:36am UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/13 "2026-02-04T07:36:22Z")

</div>

The 10^6 error for `asech(1.0)` looks bogus.

It would be better to make the plots

1. black, or at least dark blue,
2. with a larger axis labelling,
3. using thicker lines.

---

<div class="post-metadata">

### Author: ![nsajko](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nsajko/32/221187_2.png) [@nsajko](https://discourse.julialang.org/u/nsajko)
#### Post date: [February 4, 2026, 9:31am UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/14 "2026-02-04T09:31:26Z")

</div>

> [@Tamas\_Papp](#):
>
> The 10^6 error for `asech(1.0)` looks bogus.

I doubt that. In general, the error may be underestimated, but I can’t think of how it could be overestimated. A closeup, with parameters chosen to disable all smart functionality, showing the error for the rightmost hundred `Float64` numbers in the domain of `asech`:

```sh
visualize_ulp_error -t5 -- plot '(;
    parent_dir = "/home/nsajko/ulp_error_plots",
    downsampled_length = 1000, # this is excessive but there is no harm in that
    factor = 1, # disable downsampling
    window_size = 1, # disable smoothing
    bf_precision = 300, # extra high precision, just to be safe
    no_scoped_values = true,
    width = 500px,
    height = 300px,
    func = asech,
    itv = (prevfloat(1.0, 99), 1.0), # only consider the last hundred `Float64` numbers in the domain of `asech`
)'

```

Plot:

 ![asech](https://global.discourse-cdn.com/julialang/original/3X/8/b/8b3f94237bd41703a24c7b2beb994cbea16b6a9e.svg)

Reading off the plot: the error alternates between huge and small for successive floating-point numbers at the right edge of the domain. Adding this as a usage example to the readme.

Indeed, it is easy to check this in the REPL starting from first principles:

```julia-repl
julia> const asech_accurate = Float64 ∘ asech ∘ big
Float64 ∘ asech ∘ big

julia> f(x) = (asech_accurate(x), asech(x))
f (generic function with 1 method)

julia> ulp_err(a, b) = abs(b - a)/eps(a) # simplified definition, does not handle edge cases
ulp_err (generic function with 1 method)

julia> x = 1.0
1.0

julia> y = f(x)
(0.0, 0.0)

julia> x = prevfloat(x)
0.9999999999999999

julia> y = f(x)
(1.4901161193847656e-8, 2.1073424255447017e-8)

julia> ulp_err(y...)
1.865452045155277e15

```

> [@Tamas\_Papp](#):
>
> It would be better to make the plots
> 
> 1. black, or at least dark blue,
> 2. with a larger axis labelling,
> 3. using thicker lines.

Thank you. I intend to fix these issues without getting bogged down in such details of the presentation, by exporting the final data as Vega-friendly JSON, and/or relying on Vega.jl or Deneb.jl. Then the ecosystems around Vega should offer sane defaults for things like coloring, labeling, sizing, interactive zooming, etc. Issue: [JSON export? Vega export? Use Vega.jl? Use Deneb.jl? · Issue #7 · JuliaMath/VisualizeULPError.jl · GitHub](https://github.com/JuliaMath/VisualizeULPError.jl/issues/7)

---

<div class="post-metadata">

### Author: ![foobar\_lv2](https://avatars.discourse-cdn.com/v4/letter/f/ee59a6/32.png) [@foobar\_lv2](https://discourse.julialang.org/u/foobar_lv2)
#### Post date: [February 4, 2026, 10:18am UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/15 "2026-02-04T10:18:45Z")

</div>

The large error for asech comes from `x->(inv(x)-1)`.

But I don’t think this really matters? People who call `asech`/`acosh` for values close to the singularity at `1` deserve what they get, same as people who call `log` for values close to `1`.

This is a good indication that we’re missing an `acosh1p` / `asech1m` in Base/stdlib though, analogue to `log1p`. (we could also consider `inv1p`)

PS. The fix would be to implement `acosh1p` and `inv1p` and then use `asech(x)=acosh1p(inv1p)` for values close to 1.

An more relaxed definition of relative error would be: `err(f_approx, f, x) = abs(f(x)-f_approx(x)) / max(eps(x), eps(f(x)))`. I would call this the “garbage in, garbage out” principle.

---

<div class="post-metadata">

### Author: ![GunnarFarneback](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gunnarfarneback/32/1827_2.png) [@GunnarFarneback](https://discourse.julialang.org/u/GunnarFarneback)
#### Post date: [February 4, 2026, 12:08pm UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/16 "2026-02-04T12:08:47Z")

</div>

I don’t think users of `asech` deserve any worse than users of `acos`. As it happens `acos` gives a more accurate result for asech than the current Float64 `asech` implementation itself does for 0.999999991838298 \< x \< 1.

---

<div class="post-metadata">

### Author: ![foobar\_lv2](https://avatars.discourse-cdn.com/v4/letter/f/ee59a6/32.png) [@foobar\_lv2](https://discourse.julialang.org/u/foobar_lv2)
#### Post date: [February 4, 2026, 12:41pm UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/17 "2026-02-04T12:41:47Z")

</div>

Fair enough. Then we should add `invm1(t) = inv(t) - one(typeof(t))` to Base, with appropriate specializations for floating point (probably use geometric series when t is close to 1, but magic float tricks are not my forte), and use it internally where appropriate. And then we also need to add `acosh1p` to Base (such that `asech(t) = acosh1p(invm1(t))` close to 1). And then there is no good reason not to have `asech1m` as well.

---

<div class="post-metadata">

### Author: ![GunnarFarneback](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gunnarfarneback/32/1827_2.png) [@GunnarFarneback](https://discourse.julialang.org/u/GunnarFarneback)
#### Post date: [February 4, 2026, 2:38pm UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/18 "2026-02-04T14:38:14Z")

</div>

Or we could implement `asech` analogously to `acosh`, e.g. something like

```julia-auto
@noinline asech_domain_error(x) = throw(DomainError(x, "asech(x) is only defined for 0 ≤ x ≤ 1."))
function new_asech(x::T) where T <: Union{Float32, Float64}
    isnan(x) && return x

    if x < T(0) || x > T(1)
        return asech_domain_error(x)
    elseif x == T(1)
        return T(0)
    elseif x > T(0.5)
        t = T(1) - x
        return log1p((t + sqrt(T(2) * t - t * t)) / x)
    elseif x > T(0.5)^28
        y = 1 / x
        t = y * y
        return log(T(2) * y - 1 / (y + sqrt(t - T(1))))
    else
        return -log(x) + Base.Math.AH_LN2(T)
    end
end

```

As a bonus we get a better domain error and reasonable results for denormalized input.

Plot doesn’t look amazing but vastly better than today:

 ![new_asech](https://global.discourse-cdn.com/julialang/original/3X/a/a/aa00e322d84da9298888382218be12e37222daa8.svg)

---

<div class="post-metadata">

### Author: ![mikmoore](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mikmoore/32/31109_2.png) [@mikmoore](https://discourse.julialang.org/u/mikmoore)
#### Post date: [February 4, 2026, 4:17pm UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/19 "2026-02-04T16:17:25Z")

</div>

For (some) specific functions or limits of functions in certain ballparks, [Herbie](https://herbie.uwplse.org/demo/) can be a neat little tool for considering alternative implementations.

* * *

> [@foobar\_lv2](#):
>
> invm1(t) = inv(t) - one(typeof(t))

I don’t think this specific function actually needs a special implementation? For t \approx 0 the result is large and pretty accurate. For t \approx 1, the error of t^{-1} is of the same order as the input t. Do you mean \text{inv}(t+1) - 1? That one has accuracy issues, but should instead be computed as \frac{-t}{t+1} so probably doesn’t warrant a special implementation.

* * *

> [@nsajko](#):
>
> but also needs to be at least somewhat accurate.

One issue I take with these plots is that they measure the forward stability of the calculations. But I would contend that the backward stability is sometimes a better measure. I.e., is the result correct for a nearby value of the _input_. Although `asec(t)` looks really bad for t \approx 1, we see that

```julia-repl
julia> f = asec; x = nextfloat(1.0); oftype.(x, (f(x) .- f.(big(x) .+ eps(x)/2 .* (-1:1))) ./ eps(oftype(x,f(big(x)))))
3-element Vector{Float64}:
  1.8654520451552772e15
  1.0246318366302678
 -1.4314116990281882e15

```

I.e., if you evaluate the function at values _that would round to the same `Float64(t)`_, `asec(t)` can be wrong by 10^{15} ULPs. You will never get accurate values of a function near a singularity if only because of your input-rounding. One can still insist that we _ought_ to compute the output accurately with the assumption that the input is exact, but this is only situationally useful (i.e., when you actually believe your input to be exact far beyond floating point precision). So the fact that `asec` is occasionally wrong by 10^{5} ULP is not as bad as it initially sounds. Not sure as to the best way to account for this in the above analysis, however.

In any case, putting the error on a log axis would at least let us see the errors of the functions on other parts of the domain.

---

<div class="post-metadata">

### Author: ![foobar\_lv2](https://avatars.discourse-cdn.com/v4/letter/f/ee59a6/32.png) [@foobar\_lv2](https://discourse.julialang.org/u/foobar_lv2)
#### Post date: [February 4, 2026, 4:55pm UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/20 "2026-02-04T16:55:13Z")

</div>

> [@mikmoore](#):
>
> I don’t think this specific function actually needs a special implementation? For t \approx 0 the result is large and pretty accurate. For t \approx 1, the error of t^{-1} is of the same order as the input t.

If we hold it to the same extreme standards as `asech` in this thread, then it absolutely needs a special implementation. Do the same plot for `invm1 = t->inv(t)-1` and count ulps for `t = prevfloat(1.0)`.

This is where the perceived inaccuracy for asech come from: It forwards to `acosh(inv(t))`, which applies a good formula involving log1p to `t-1`.

The entire thing is of course somewhat silly – garbage in, garbage out, just as you stated in different words.

But as @GunnarFarneback correctly remarked, even users who are unwise about their stdlib usage deserve a good effort on our side. (examples of unwise calls are `log(1+epsilon)`, `acosh(1+epsilon)` or, the point of contention in this thread, `asech(1-epsilon)`. Such a call is unwise because most significant digits of `epsilon` are lost before we get to see them)

PS. edited for flagged language. Apologies, I got carried away.

---

<div class="post-metadata">

### Author: ![AntonReinhard](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/antonreinhard/32/211306_2.png) [@AntonReinhard](https://discourse.julialang.org/u/AntonReinhard)
#### Post date: [February 6, 2026, 12:40pm UTC](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392/21 "2026-02-06T12:40:37Z")

</div>

Just a shameless self-plug, I made a package PrecisionCarriers.jl a few months ago, with the very similar purpose of finding precision loss at specific numeric values, providing an interface to “benchmark” arbitrary functions easily.

Maybe the two packages could be coupled or merged in some way?  
I also made a post on [discourse about it before](https://discourse.julialang.org/t/ann-precisioncarriers-jl-easy-detection-of-floating-point-precision-loss/130670).

[Next page](https://discourse.julialang.org/t/floating-point-accuracy-visualization-example-the-ulp-error-for-each-math-function-coming-with-julia/135392.md?page=2)
