Usage Example

Suppose we have the problem of computing the square root of the positive difference of two squares, x and y. The formula is simple: $\sqrt{\left|x^2 - y^2\right|}$. Let's write it in julia:

f(x, y) = sqrt(abs(x^2 - y^2))

f(5, 4)
3.0

So far so good. Now what happens when $x$ and $y$ have almost equal values?

f(3.0 + 1.0e-7, 3.0)
0.0007745966749141229

Compare that with a result in arbitrary precision:

f(big(3.0 + 1.0e-7), big(3.0))
0.0007745966750626113059493756986839639762994675257448567639441517760627581981642562

Clearly, not all of the supposed ca. 15 digits that the Float64 result carries are correct. Let's see what PrecisionCarriers says:

using PrecisionCarriers
p = f(precify(3.0 + 1.0e-7), precify(3.0))
0.0007745966749141229 <ε=863330>
significant_digits(p)
9.71738243893589

It looks like we lost about 5 significant digits! This happens because of the intermediate results of $x^2$ and $y^2$ do not carry enough precision to accurately calculate their difference. This is often called "catastrophic cancellation", because the two values are almost equal, so many of the most-significant bits are "cancelled".

In this instance, we can resolve the problem for most cases by replacing $x^2 - y^2$ with its binomial representation $(x + y) * (x - y)$. This reduces the instability of the intermediate values:

f_improved(x, y) = sqrt(abs((x + y) * (x - y)))
p = f_improved(precify(3.0 + 1.0e-7), precify(3.0))
0.0007745966750626113 <ε=0>
significant_digits(p)
15.653559774527022

We can now also easily visualize the precision loss of either version by plotting the significant digits on an x-y-plane:

using CairoMakie

x = 1.0:1.0e-6:(1.0 + 1.0e-4)
y = 1.0:1.0e-6:(1.0 + 1.0e-4)

contourf(x, y, (x, y) -> significant_digits(f(precify(x), precify(y))))
Example block output

With a little more effort we can compare the two versions on some values:

fig = Figure()

z1 = [significant_digits(f(precify(xi), precify(yi))) for yi in y, xi in x]
z2 = [significant_digits(f_improved(precify(xi), precify(yi))) for yi in y, xi in x]

zmin = floor(Int64, min(minimum(z1), minimum(z2)))
zmax = ceil(Int64, max(maximum(z1), maximum(z2)))

ax1 = Axis(fig[1, 1]; aspect = AxisAspect(1), title = "Original f", xticksvisible = false, yticksvisible = false, xticklabelsvisible = false, yticklabelsvisible = false)
ax2 = Axis(fig[1, 2]; aspect = AxisAspect(1), title = "Improved f", xticksvisible = false, yticksvisible = false, xticklabelsvisible = false, yticklabelsvisible = false)

contour1 = contourf!(ax1, x, y, z1; levels = range(zmin, zmax))
contour2 = contourf!(ax2, x, y, z2; levels = range(zmin, zmax))

Colorbar(fig[1, 3], contour1; label = "Significant Digits")

fig
Example block output

This page was generated using Literate.jl.